@vizzly-testing/cli 0.10.3 → 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.
- package/.claude-plugin/.mcp.json +8 -0
- package/.claude-plugin/README.md +114 -0
- package/.claude-plugin/commands/debug-diff.md +153 -0
- package/.claude-plugin/commands/setup.md +137 -0
- package/.claude-plugin/commands/suggest-screenshots.md +111 -0
- package/.claude-plugin/commands/tdd-status.md +43 -0
- package/.claude-plugin/marketplace.json +28 -0
- package/.claude-plugin/mcp/vizzly-server/cloud-api-provider.js +354 -0
- package/.claude-plugin/mcp/vizzly-server/index.js +861 -0
- package/.claude-plugin/mcp/vizzly-server/local-tdd-provider.js +422 -0
- package/.claude-plugin/mcp/vizzly-server/token-resolver.js +185 -0
- package/.claude-plugin/plugin.json +14 -0
- package/README.md +168 -8
- package/dist/cli.js +64 -0
- package/dist/client/index.js +13 -3
- package/dist/commands/login.js +195 -0
- package/dist/commands/logout.js +71 -0
- package/dist/commands/project.js +351 -0
- package/dist/commands/run.js +30 -0
- package/dist/commands/whoami.js +162 -0
- package/dist/plugin-loader.js +4 -2
- package/dist/sdk/index.js +16 -4
- package/dist/services/api-service.js +50 -7
- package/dist/services/auth-service.js +226 -0
- package/dist/types/client/index.d.ts +9 -3
- package/dist/types/commands/login.d.ts +11 -0
- package/dist/types/commands/logout.d.ts +11 -0
- package/dist/types/commands/project.d.ts +28 -0
- package/dist/types/commands/whoami.d.ts +11 -0
- package/dist/types/sdk/index.d.ts +9 -4
- package/dist/types/services/api-service.d.ts +2 -1
- package/dist/types/services/auth-service.d.ts +59 -0
- package/dist/types/utils/browser.d.ts +6 -0
- package/dist/types/utils/config-loader.d.ts +1 -1
- package/dist/types/utils/config-schema.d.ts +8 -174
- package/dist/types/utils/file-helpers.d.ts +18 -0
- package/dist/types/utils/global-config.d.ts +84 -0
- package/dist/utils/browser.js +44 -0
- package/dist/utils/config-loader.js +69 -3
- package/dist/utils/file-helpers.js +64 -0
- package/dist/utils/global-config.js +259 -0
- package/docs/api-reference.md +177 -6
- package/docs/authentication.md +334 -0
- package/docs/getting-started.md +21 -2
- package/docs/plugins.md +27 -0
- package/docs/test-integration.md +60 -10
- 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);
|