@vizzly-testing/cli 0.10.2 → 0.11.0

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 (48) hide show
  1. package/.claude-plugin/.mcp.json +8 -0
  2. package/.claude-plugin/README.md +114 -0
  3. package/.claude-plugin/commands/debug-diff.md +153 -0
  4. package/.claude-plugin/commands/setup.md +137 -0
  5. package/.claude-plugin/commands/suggest-screenshots.md +111 -0
  6. package/.claude-plugin/commands/tdd-status.md +43 -0
  7. package/.claude-plugin/marketplace.json +28 -0
  8. package/.claude-plugin/mcp/vizzly-server/cloud-api-provider.js +354 -0
  9. package/.claude-plugin/mcp/vizzly-server/index.js +861 -0
  10. package/.claude-plugin/mcp/vizzly-server/local-tdd-provider.js +422 -0
  11. package/.claude-plugin/mcp/vizzly-server/token-resolver.js +185 -0
  12. package/.claude-plugin/plugin.json +14 -0
  13. package/README.md +168 -8
  14. package/dist/cli.js +64 -0
  15. package/dist/client/index.js +13 -3
  16. package/dist/commands/login.js +195 -0
  17. package/dist/commands/logout.js +71 -0
  18. package/dist/commands/project.js +351 -0
  19. package/dist/commands/run.js +30 -0
  20. package/dist/commands/whoami.js +162 -0
  21. package/dist/plugin-loader.js +4 -2
  22. package/dist/sdk/index.js +16 -4
  23. package/dist/services/api-service.js +50 -7
  24. package/dist/services/auth-service.js +226 -0
  25. package/dist/services/tdd-service.js +2 -1
  26. package/dist/types/client/index.d.ts +9 -3
  27. package/dist/types/commands/login.d.ts +11 -0
  28. package/dist/types/commands/logout.d.ts +11 -0
  29. package/dist/types/commands/project.d.ts +28 -0
  30. package/dist/types/commands/whoami.d.ts +11 -0
  31. package/dist/types/sdk/index.d.ts +9 -4
  32. package/dist/types/services/api-service.d.ts +2 -1
  33. package/dist/types/services/auth-service.d.ts +59 -0
  34. package/dist/types/utils/browser.d.ts +6 -0
  35. package/dist/types/utils/config-loader.d.ts +1 -1
  36. package/dist/types/utils/config-schema.d.ts +8 -174
  37. package/dist/types/utils/file-helpers.d.ts +18 -0
  38. package/dist/types/utils/global-config.d.ts +84 -0
  39. package/dist/utils/browser.js +44 -0
  40. package/dist/utils/config-loader.js +69 -3
  41. package/dist/utils/file-helpers.js +64 -0
  42. package/dist/utils/global-config.js +259 -0
  43. package/docs/api-reference.md +177 -6
  44. package/docs/authentication.md +334 -0
  45. package/docs/getting-started.md +21 -2
  46. package/docs/plugins.md +27 -0
  47. package/docs/test-integration.md +60 -10
  48. package/package.json +5 -3
@@ -0,0 +1,861 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Vizzly MCP Server
5
+ * Provides Claude Code with access to Vizzly TDD state and cloud API
6
+ */
7
+
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import {
11
+ CallToolRequestSchema,
12
+ ListToolsRequestSchema,
13
+ ListResourcesRequestSchema,
14
+ ReadResourceRequestSchema
15
+ } from '@modelcontextprotocol/sdk/types.js';
16
+ import { LocalTDDProvider } from './local-tdd-provider.js';
17
+ import { CloudAPIProvider } from './cloud-api-provider.js';
18
+ import { resolveToken, getUserInfo } from './token-resolver.js';
19
+
20
+ class VizzlyMCPServer {
21
+ constructor() {
22
+ this.server = new Server(
23
+ {
24
+ name: 'vizzly-server',
25
+ version: '0.1.0'
26
+ },
27
+ {
28
+ capabilities: {
29
+ tools: {},
30
+ resources: {}
31
+ }
32
+ }
33
+ );
34
+
35
+ this.localProvider = new LocalTDDProvider();
36
+ this.cloudProvider = new CloudAPIProvider();
37
+
38
+ this.setupHandlers();
39
+ }
40
+
41
+ /**
42
+ * Detect context - whether user is working locally or with cloud builds
43
+ */
44
+ async detectContext(workingDirectory) {
45
+ let context = {
46
+ hasLocalTDD: false,
47
+ hasApiToken: false,
48
+ hasAuthentication: false,
49
+ mode: null,
50
+ tokenSource: null,
51
+ user: null
52
+ };
53
+
54
+ // Check for local TDD setup
55
+ let vizzlyDir = await this.localProvider.findVizzlyDir(workingDirectory);
56
+ if (vizzlyDir) {
57
+ context.hasLocalTDD = true;
58
+ context.mode = 'local';
59
+ }
60
+
61
+ // Check for API token using token resolver
62
+ let token = await resolveToken({ workingDirectory });
63
+ if (token) {
64
+ context.hasApiToken = true;
65
+ context.hasAuthentication = true;
66
+
67
+ // Determine token source
68
+ if (process.env.VIZZLY_TOKEN) {
69
+ context.tokenSource = 'environment';
70
+ } else if (token.startsWith('vzt_')) {
71
+ context.tokenSource = 'project_mapping';
72
+ } else {
73
+ context.tokenSource = 'user_login';
74
+ // Include user info if available
75
+ let userInfo = await getUserInfo();
76
+ if (userInfo) {
77
+ context.user = {
78
+ email: userInfo.email,
79
+ name: userInfo.name
80
+ };
81
+ }
82
+ }
83
+
84
+ if (!context.mode) {
85
+ context.mode = 'cloud';
86
+ }
87
+ }
88
+
89
+ return context;
90
+ }
91
+
92
+ /**
93
+ * Resolve API token from various sources
94
+ * @param {Object} args - Tool arguments that may contain apiToken
95
+ * @param {string} args.apiToken - Explicitly provided token
96
+ * @param {string} args.workingDirectory - Working directory for project mapping
97
+ * @returns {Promise<string|null>} Resolved token
98
+ */
99
+ async resolveApiToken(args = {}) {
100
+ return await resolveToken({
101
+ providedToken: args.apiToken,
102
+ workingDirectory: args.workingDirectory || process.cwd()
103
+ });
104
+ }
105
+
106
+ setupHandlers() {
107
+ // List available tools
108
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
109
+ tools: [
110
+ {
111
+ name: 'detect_context',
112
+ description:
113
+ "Detect whether user is working in local TDD mode or cloud collaboration mode. Use this at the start of a conversation to understand the user's workflow.",
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ workingDirectory: {
118
+ type: 'string',
119
+ description: 'Path to project directory (optional)'
120
+ }
121
+ }
122
+ }
123
+ },
124
+ {
125
+ name: 'get_tdd_status',
126
+ description:
127
+ 'Get current TDD mode status including comparison results, failed/new/passed counts, and diff information',
128
+ inputSchema: {
129
+ type: 'object',
130
+ properties: {
131
+ workingDirectory: {
132
+ type: 'string',
133
+ description: 'Path to project directory (optional, defaults to current directory)'
134
+ }
135
+ }
136
+ }
137
+ },
138
+ {
139
+ name: 'read_comparison_details',
140
+ description:
141
+ 'Read detailed comparison information for a screenshot or comparison. Automatically detects if working in local TDD mode or cloud mode. Pass either a screenshot name (e.g., "homepage_desktop") for local mode or a comparison ID (e.g., "cmp_abc123") for cloud mode. IMPORTANT: Returns both paths (for local) and URLs (for cloud). Use Read tool for paths, WebFetch for URLs. Do NOT read/fetch diff images as they cause API errors.',
142
+ inputSchema: {
143
+ type: 'object',
144
+ properties: {
145
+ identifier: {
146
+ type: 'string',
147
+ description: 'Screenshot name (local mode) or comparison ID (cloud mode)'
148
+ },
149
+ workingDirectory: {
150
+ type: 'string',
151
+ description: 'Path to project directory (optional, for local mode)'
152
+ },
153
+ apiToken: {
154
+ type: 'string',
155
+ description:
156
+ 'Vizzly API token (optional, auto-resolves from: CLI flag > env var > project mapping > user login)'
157
+ },
158
+ apiUrl: {
159
+ type: 'string',
160
+ description: 'API base URL (optional, for cloud mode)'
161
+ }
162
+ },
163
+ required: ['identifier']
164
+ }
165
+ },
166
+ {
167
+ name: 'list_diff_images',
168
+ description:
169
+ 'List all available diff images from TDD comparisons. Returns paths for reference only - do NOT read these images as they cause API errors. Use read_comparison_details to get baseline and current image paths instead.',
170
+ inputSchema: {
171
+ type: 'object',
172
+ properties: {
173
+ workingDirectory: {
174
+ type: 'string',
175
+ description: 'Path to project directory (optional)'
176
+ }
177
+ }
178
+ }
179
+ },
180
+ {
181
+ name: 'get_build_status',
182
+ description: 'Get cloud build status and comparison results (requires API token)',
183
+ inputSchema: {
184
+ type: 'object',
185
+ properties: {
186
+ buildId: {
187
+ type: 'string',
188
+ description: 'Build ID to check status for'
189
+ },
190
+ apiToken: {
191
+ type: 'string',
192
+ description: 'Vizzly API token (optional, auto-resolves from user login or env)'
193
+ },
194
+ apiUrl: {
195
+ type: 'string',
196
+ description: 'API base URL (optional, defaults to https://app.vizzly.dev)'
197
+ }
198
+ },
199
+ required: ['buildId']
200
+ }
201
+ },
202
+ {
203
+ name: 'list_recent_builds',
204
+ description: 'List recent builds from Vizzly cloud (requires API token)',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ limit: {
209
+ type: 'number',
210
+ description: 'Number of builds to return (default: 10)'
211
+ },
212
+ branch: {
213
+ type: 'string',
214
+ description: 'Filter by branch name (optional)'
215
+ },
216
+ apiToken: {
217
+ type: 'string',
218
+ description: 'Vizzly API token (optional, auto-resolves from user login or env)'
219
+ },
220
+ apiUrl: {
221
+ type: 'string',
222
+ description: 'API base URL (optional)'
223
+ }
224
+ }
225
+ }
226
+ },
227
+ {
228
+ name: 'get_comparison',
229
+ description:
230
+ 'Get detailed comparison information from cloud API including screenshot URLs. IMPORTANT: Use WebFetch to view ONLY baselineUrl and currentUrl - do NOT fetch diffUrl as it causes API errors.',
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: {
234
+ comparisonId: {
235
+ type: 'string',
236
+ description: 'Comparison ID'
237
+ },
238
+ apiToken: {
239
+ type: 'string',
240
+ description: 'Vizzly API token (optional, auto-resolves from user login or env)'
241
+ },
242
+ apiUrl: {
243
+ type: 'string',
244
+ description: 'API base URL (optional)'
245
+ }
246
+ },
247
+ required: ['comparisonId']
248
+ }
249
+ },
250
+ {
251
+ name: 'create_build_comment',
252
+ description: 'Create a comment on a build for collaboration',
253
+ inputSchema: {
254
+ type: 'object',
255
+ properties: {
256
+ buildId: {
257
+ type: 'string',
258
+ description: 'Build ID to comment on'
259
+ },
260
+ content: {
261
+ type: 'string',
262
+ description: 'Comment text content'
263
+ },
264
+ type: {
265
+ type: 'string',
266
+ description: 'Comment type: general, approval, rejection (default: general)'
267
+ },
268
+ apiToken: {
269
+ type: 'string',
270
+ description: 'Vizzly API token (optional, auto-resolves from user login or env)'
271
+ },
272
+ apiUrl: {
273
+ type: 'string',
274
+ description: 'API base URL (optional)'
275
+ }
276
+ },
277
+ required: ['buildId', 'content']
278
+ }
279
+ },
280
+ {
281
+ name: 'list_build_comments',
282
+ description: 'List all comments on a build',
283
+ inputSchema: {
284
+ type: 'object',
285
+ properties: {
286
+ buildId: {
287
+ type: 'string',
288
+ description: 'Build ID'
289
+ },
290
+ apiToken: {
291
+ type: 'string',
292
+ description: 'Vizzly API token (optional, auto-resolves from user login or env)'
293
+ },
294
+ apiUrl: {
295
+ type: 'string',
296
+ description: 'API base URL (optional)'
297
+ }
298
+ },
299
+ required: ['buildId']
300
+ }
301
+ },
302
+ {
303
+ name: 'approve_comparison',
304
+ description: 'Approve a comparison - indicates the visual change is acceptable',
305
+ inputSchema: {
306
+ type: 'object',
307
+ properties: {
308
+ comparisonId: {
309
+ type: 'string',
310
+ description: 'Comparison ID to approve'
311
+ },
312
+ comment: {
313
+ type: 'string',
314
+ description: 'Optional comment explaining the approval'
315
+ },
316
+ apiToken: {
317
+ type: 'string',
318
+ description: 'Vizzly API token (optional, auto-resolves from user login or env)'
319
+ },
320
+ apiUrl: {
321
+ type: 'string',
322
+ description: 'API base URL (optional)'
323
+ }
324
+ },
325
+ required: ['comparisonId']
326
+ }
327
+ },
328
+ {
329
+ name: 'reject_comparison',
330
+ description: 'Reject a comparison - indicates the visual change needs fixing',
331
+ inputSchema: {
332
+ type: 'object',
333
+ properties: {
334
+ comparisonId: {
335
+ type: 'string',
336
+ description: 'Comparison ID to reject'
337
+ },
338
+ reason: {
339
+ type: 'string',
340
+ description: 'Required reason for rejection (will be added as a comment)'
341
+ },
342
+ apiToken: {
343
+ type: 'string',
344
+ description: 'Vizzly API token (optional, auto-resolves from user login or env)'
345
+ },
346
+ apiUrl: {
347
+ type: 'string',
348
+ description: 'API base URL (optional)'
349
+ }
350
+ },
351
+ required: ['comparisonId', 'reason']
352
+ }
353
+ },
354
+ {
355
+ name: 'get_review_summary',
356
+ description: 'Get review status and assignments for a build',
357
+ inputSchema: {
358
+ type: 'object',
359
+ properties: {
360
+ buildId: {
361
+ type: 'string',
362
+ description: 'Build ID'
363
+ },
364
+ apiToken: {
365
+ type: 'string',
366
+ description: 'Vizzly API token (optional, auto-resolves from user login or env)'
367
+ },
368
+ apiUrl: {
369
+ type: 'string',
370
+ description: 'API base URL (optional)'
371
+ }
372
+ },
373
+ required: ['buildId']
374
+ }
375
+ },
376
+ {
377
+ name: 'accept_baseline',
378
+ description: 'Accept a screenshot as the new baseline in local TDD mode',
379
+ inputSchema: {
380
+ type: 'object',
381
+ properties: {
382
+ screenshotName: {
383
+ type: 'string',
384
+ description: 'Name of the screenshot to accept'
385
+ },
386
+ workingDirectory: {
387
+ type: 'string',
388
+ description: 'Path to project directory (optional)'
389
+ }
390
+ },
391
+ required: ['screenshotName']
392
+ }
393
+ },
394
+ {
395
+ name: 'reject_baseline',
396
+ description:
397
+ 'Reject a screenshot baseline in local TDD mode (marks it for investigation)',
398
+ inputSchema: {
399
+ type: 'object',
400
+ properties: {
401
+ screenshotName: {
402
+ type: 'string',
403
+ description: 'Name of the screenshot to reject'
404
+ },
405
+ reason: {
406
+ type: 'string',
407
+ description: 'Reason for rejection'
408
+ },
409
+ workingDirectory: {
410
+ type: 'string',
411
+ description: 'Path to project directory (optional)'
412
+ }
413
+ },
414
+ required: ['screenshotName', 'reason']
415
+ }
416
+ },
417
+ {
418
+ name: 'download_baselines',
419
+ description: 'Download baseline screenshots from a cloud build to use in local TDD mode',
420
+ inputSchema: {
421
+ type: 'object',
422
+ properties: {
423
+ buildId: {
424
+ type: 'string',
425
+ description: 'Build ID to download baselines from'
426
+ },
427
+ screenshotNames: {
428
+ type: 'array',
429
+ items: {
430
+ type: 'string'
431
+ },
432
+ description:
433
+ 'Optional list of specific screenshot names to download (if not provided, downloads all)'
434
+ },
435
+ apiToken: {
436
+ type: 'string',
437
+ description: 'Vizzly API token (optional, auto-resolves from user login or env)'
438
+ },
439
+ apiUrl: {
440
+ type: 'string',
441
+ description: 'API base URL (optional)'
442
+ },
443
+ workingDirectory: {
444
+ type: 'string',
445
+ description: 'Path to project directory (optional)'
446
+ }
447
+ },
448
+ required: ['buildId']
449
+ }
450
+ }
451
+ ]
452
+ }));
453
+
454
+ // List available resources
455
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
456
+ resources: [
457
+ {
458
+ uri: 'vizzly://tdd/status',
459
+ name: 'TDD Status',
460
+ description: 'Current TDD comparison results and statistics',
461
+ mimeType: 'application/json'
462
+ },
463
+ {
464
+ uri: 'vizzly://tdd/server-info',
465
+ name: 'TDD Server Info',
466
+ description: 'Information about the running TDD server',
467
+ mimeType: 'application/json'
468
+ }
469
+ ]
470
+ }));
471
+
472
+ // Read resource content
473
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
474
+ let { uri } = request.params;
475
+
476
+ if (uri === 'vizzly://tdd/status') {
477
+ let status = await this.localProvider.getTDDStatus();
478
+ return {
479
+ contents: [
480
+ {
481
+ uri,
482
+ mimeType: 'application/json',
483
+ text: JSON.stringify(status, null, 2)
484
+ }
485
+ ]
486
+ };
487
+ }
488
+
489
+ if (uri === 'vizzly://tdd/server-info') {
490
+ let serverInfo = await this.localProvider.getServerInfo();
491
+ return {
492
+ contents: [
493
+ {
494
+ uri,
495
+ mimeType: 'application/json',
496
+ text: JSON.stringify(serverInfo, null, 2)
497
+ }
498
+ ]
499
+ };
500
+ }
501
+
502
+ throw new Error(`Unknown resource: ${uri}`);
503
+ });
504
+
505
+ // Handle tool calls
506
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
507
+ let { name, arguments: args } = request.params;
508
+
509
+ try {
510
+ switch (name) {
511
+ case 'detect_context': {
512
+ let context = await this.detectContext(args.workingDirectory);
513
+ return {
514
+ content: [
515
+ {
516
+ type: 'text',
517
+ text: JSON.stringify(context, null, 2)
518
+ }
519
+ ]
520
+ };
521
+ }
522
+
523
+ case 'get_tdd_status': {
524
+ let status = await this.localProvider.getTDDStatus(args.workingDirectory);
525
+ return {
526
+ content: [
527
+ {
528
+ type: 'text',
529
+ text: JSON.stringify(status, null, 2)
530
+ }
531
+ ]
532
+ };
533
+ }
534
+
535
+ case 'read_comparison_details': {
536
+ // Unified handler that tries local first, then cloud
537
+ let details = null;
538
+ let mode = null;
539
+
540
+ // Try local mode first (if vizzlyDir exists)
541
+ try {
542
+ details = await this.localProvider.getComparisonDetails(
543
+ args.identifier,
544
+ args.workingDirectory
545
+ );
546
+ mode = 'local';
547
+ } catch (localError) {
548
+ // If local fails and we have API token, try cloud mode
549
+ let apiToken = await this.resolveApiToken(args);
550
+ if (apiToken && args.identifier.startsWith('cmp_')) {
551
+ try {
552
+ let cloudComparison = await this.cloudProvider.getComparison(
553
+ args.identifier,
554
+ apiToken,
555
+ args.apiUrl
556
+ );
557
+
558
+ // Transform cloud response to unified format
559
+ details = {
560
+ mode: 'cloud',
561
+ name: cloudComparison.name,
562
+ status: cloudComparison.status,
563
+ diffPercentage: cloudComparison.diff_percentage,
564
+ threshold: cloudComparison.threshold,
565
+ hasDiff: cloudComparison.has_diff,
566
+ // Cloud URLs
567
+ baselineUrl: cloudComparison.baseline_screenshot?.original_url,
568
+ currentUrl: cloudComparison.current_screenshot?.original_url,
569
+ diffUrl: cloudComparison.diff_image?.url,
570
+ // Additional cloud metadata
571
+ comparisonId: cloudComparison.id,
572
+ buildId: cloudComparison.build_id,
573
+ analysis: [
574
+ `Cloud comparison (${cloudComparison.diff_percentage?.toFixed(2)}% difference)`,
575
+ 'Use WebFetch to view baselineUrl and currentUrl',
576
+ 'Do NOT fetch diffUrl as it causes API errors'
577
+ ]
578
+ };
579
+ mode = 'cloud';
580
+ } catch (cloudError) {
581
+ throw new Error(
582
+ `Failed to get comparison details: Local - ${localError.message}, Cloud - ${cloudError.message}`
583
+ );
584
+ }
585
+ } else {
586
+ throw localError;
587
+ }
588
+ }
589
+
590
+ // Add mode to response if not already present
591
+ if (mode && !details.mode) {
592
+ details.mode = mode;
593
+ }
594
+
595
+ return {
596
+ content: [
597
+ {
598
+ type: 'text',
599
+ text: JSON.stringify(details, null, 2)
600
+ }
601
+ ]
602
+ };
603
+ }
604
+
605
+ case 'list_diff_images': {
606
+ let diffs = await this.localProvider.listDiffImages(args.workingDirectory);
607
+ return {
608
+ content: [
609
+ {
610
+ type: 'text',
611
+ text: JSON.stringify(diffs, null, 2)
612
+ }
613
+ ]
614
+ };
615
+ }
616
+
617
+ case 'get_build_status': {
618
+ let apiToken = await this.resolveApiToken(args);
619
+ let buildStatus = await this.cloudProvider.getBuildStatus(
620
+ args.buildId,
621
+ apiToken,
622
+ args.apiUrl
623
+ );
624
+ return {
625
+ content: [
626
+ {
627
+ type: 'text',
628
+ text: JSON.stringify(buildStatus, null, 2)
629
+ }
630
+ ]
631
+ };
632
+ }
633
+
634
+ case 'list_recent_builds': {
635
+ let apiToken = await this.resolveApiToken(args);
636
+ let builds = await this.cloudProvider.listRecentBuilds(
637
+ apiToken,
638
+ {
639
+ limit: args.limit,
640
+ branch: args.branch,
641
+ apiUrl: args.apiUrl
642
+ }
643
+ );
644
+ return {
645
+ content: [
646
+ {
647
+ type: 'text',
648
+ text: JSON.stringify(builds, null, 2)
649
+ }
650
+ ]
651
+ };
652
+ }
653
+
654
+ case 'get_comparison': {
655
+ let apiToken = await this.resolveApiToken(args);
656
+ let comparison = await this.cloudProvider.getComparison(
657
+ args.comparisonId,
658
+ apiToken,
659
+ args.apiUrl
660
+ );
661
+ return {
662
+ content: [
663
+ {
664
+ type: 'text',
665
+ text: JSON.stringify(comparison, null, 2)
666
+ }
667
+ ]
668
+ };
669
+ }
670
+
671
+ case 'create_build_comment': {
672
+ let apiToken = await this.resolveApiToken(args);
673
+ let result = await this.cloudProvider.createBuildComment(
674
+ args.buildId,
675
+ args.content,
676
+ args.type || 'general',
677
+ apiToken,
678
+ args.apiUrl
679
+ );
680
+ return {
681
+ content: [
682
+ {
683
+ type: 'text',
684
+ text: JSON.stringify(result, null, 2)
685
+ }
686
+ ]
687
+ };
688
+ }
689
+
690
+ case 'list_build_comments': {
691
+ let apiToken = await this.resolveApiToken(args);
692
+ let comments = await this.cloudProvider.listBuildComments(
693
+ args.buildId,
694
+ apiToken,
695
+ args.apiUrl
696
+ );
697
+ return {
698
+ content: [
699
+ {
700
+ type: 'text',
701
+ text: JSON.stringify(comments, null, 2)
702
+ }
703
+ ]
704
+ };
705
+ }
706
+
707
+ case 'approve_comparison': {
708
+ let apiToken = await this.resolveApiToken(args);
709
+ let result = await this.cloudProvider.approveComparison(
710
+ args.comparisonId,
711
+ args.comment,
712
+ apiToken,
713
+ args.apiUrl
714
+ );
715
+ return {
716
+ content: [
717
+ {
718
+ type: 'text',
719
+ text: JSON.stringify(result, null, 2)
720
+ }
721
+ ]
722
+ };
723
+ }
724
+
725
+ case 'reject_comparison': {
726
+ let apiToken = await this.resolveApiToken(args);
727
+ let result = await this.cloudProvider.rejectComparison(
728
+ args.comparisonId,
729
+ args.reason,
730
+ apiToken,
731
+ args.apiUrl
732
+ );
733
+ return {
734
+ content: [
735
+ {
736
+ type: 'text',
737
+ text: JSON.stringify(result, null, 2)
738
+ }
739
+ ]
740
+ };
741
+ }
742
+
743
+ case 'get_review_summary': {
744
+ let apiToken = await this.resolveApiToken(args);
745
+ let summary = await this.cloudProvider.getReviewSummary(
746
+ args.buildId,
747
+ apiToken,
748
+ args.apiUrl
749
+ );
750
+ return {
751
+ content: [
752
+ {
753
+ type: 'text',
754
+ text: JSON.stringify(summary, null, 2)
755
+ }
756
+ ]
757
+ };
758
+ }
759
+
760
+ case 'accept_baseline': {
761
+ let result = await this.localProvider.acceptBaseline(
762
+ args.screenshotName,
763
+ args.workingDirectory
764
+ );
765
+ return {
766
+ content: [
767
+ {
768
+ type: 'text',
769
+ text: JSON.stringify(result, null, 2)
770
+ }
771
+ ]
772
+ };
773
+ }
774
+
775
+ case 'reject_baseline': {
776
+ let result = await this.localProvider.rejectBaseline(
777
+ args.screenshotName,
778
+ args.reason,
779
+ args.workingDirectory
780
+ );
781
+ return {
782
+ content: [
783
+ {
784
+ type: 'text',
785
+ text: JSON.stringify(result, null, 2)
786
+ }
787
+ ]
788
+ };
789
+ }
790
+
791
+ case 'download_baselines': {
792
+ // First get build metadata and screenshot data from cloud
793
+ let apiToken = await this.resolveApiToken(args);
794
+
795
+ // Get full build status for metadata
796
+ let buildStatus = await this.cloudProvider.getBuildStatus(
797
+ args.buildId,
798
+ apiToken,
799
+ args.apiUrl
800
+ );
801
+
802
+ // Get screenshot data
803
+ let cloudData = await this.cloudProvider.downloadBaselines(
804
+ args.buildId,
805
+ args.screenshotNames,
806
+ apiToken,
807
+ args.apiUrl
808
+ );
809
+
810
+ // Download and save locally with build metadata
811
+ let result = await this.localProvider.downloadBaselinesFromCloud(
812
+ cloudData.screenshots,
813
+ args.workingDirectory,
814
+ buildStatus.build // Pass build metadata
815
+ );
816
+
817
+ return {
818
+ content: [
819
+ {
820
+ type: 'text',
821
+ text: JSON.stringify(
822
+ {
823
+ buildId: cloudData.buildId,
824
+ buildName: cloudData.buildName,
825
+ ...result
826
+ },
827
+ null,
828
+ 2
829
+ )
830
+ }
831
+ ]
832
+ };
833
+ }
834
+
835
+ default:
836
+ throw new Error(`Unknown tool: ${name}`);
837
+ }
838
+ } catch (error) {
839
+ return {
840
+ content: [
841
+ {
842
+ type: 'text',
843
+ text: `Error: ${error.message}`
844
+ }
845
+ ],
846
+ isError: true
847
+ };
848
+ }
849
+ });
850
+ }
851
+
852
+ async run() {
853
+ let transport = new StdioServerTransport();
854
+ await this.server.connect(transport);
855
+ console.error('Vizzly MCP server running on stdio');
856
+ }
857
+ }
858
+
859
+ // Start server
860
+ let server = new VizzlyMCPServer();
861
+ server.run().catch(console.error);