viewgate-mcp 1.0.43 → 1.0.45

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 (2) hide show
  1. package/dist/index.js +247 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -46,6 +46,187 @@ function createMcpServer(apiKey, personalKey) {
46
46
  tools: {},
47
47
  },
48
48
  });
49
+ const FLOW_TTL_MS = 10 * 60 * 1000;
50
+ const guard = {
51
+ flow: "idle",
52
+ step: 0,
53
+ startedAt: Date.now(),
54
+ lastActivityAt: Date.now(),
55
+ };
56
+ function resetGuard() {
57
+ guard.flow = "idle";
58
+ guard.step = 0;
59
+ guard.lastTool = undefined;
60
+ guard.startedAt = Date.now();
61
+ guard.lastActivityAt = Date.now();
62
+ }
63
+ function getToolFlow(toolName) {
64
+ switch (toolName) {
65
+ case "get_ui_components":
66
+ case "generate_ui_components":
67
+ case "mark_ui_component_generated":
68
+ return "ui_components";
69
+ case "get_annotations":
70
+ case "mark_annotation_ready":
71
+ case "mark_annotations_as_live":
72
+ return "annotations";
73
+ case "get_ui_improvements":
74
+ return "ui_improvements";
75
+ case "planning":
76
+ return "planning";
77
+ case "sync_endpoints":
78
+ case "get_synced_endpoints":
79
+ return "sync";
80
+ case "get_ai_resolved_tickets":
81
+ return "idle";
82
+ default:
83
+ return "idle";
84
+ }
85
+ }
86
+ function enforceWorkflow(toolName, args) {
87
+ const now = Date.now();
88
+ if (guard.flow !== "idle" && now - guard.lastActivityAt > FLOW_TTL_MS) {
89
+ resetGuard();
90
+ }
91
+ const desiredFlow = getToolFlow(toolName);
92
+ if (toolName === "get_ai_resolved_tickets") {
93
+ if (guard.flow !== "idle") {
94
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
95
+ }
96
+ guard.lastTool = toolName;
97
+ guard.lastActivityAt = now;
98
+ return;
99
+ }
100
+ if (guard.flow === "idle") {
101
+ switch (toolName) {
102
+ case "get_ui_components":
103
+ guard.flow = "ui_components";
104
+ guard.step = 1;
105
+ break;
106
+ case "get_annotations":
107
+ guard.flow = "annotations";
108
+ guard.step = 1;
109
+ break;
110
+ case "get_ui_improvements":
111
+ guard.flow = "ui_improvements";
112
+ guard.step = 1;
113
+ break;
114
+ case "planning":
115
+ if (args?.results) {
116
+ throw new Error("TOOL_CALL_BLOCKED: planning submit requires prior fetch");
117
+ }
118
+ guard.flow = "planning";
119
+ guard.step = 1;
120
+ break;
121
+ case "sync_endpoints":
122
+ guard.flow = "sync";
123
+ guard.step = 1;
124
+ break;
125
+ default:
126
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in idle");
127
+ }
128
+ guard.startedAt = now;
129
+ guard.lastTool = toolName;
130
+ guard.lastActivityAt = now;
131
+ return;
132
+ }
133
+ if (desiredFlow !== guard.flow) {
134
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
135
+ }
136
+ if (guard.flow === "ui_components") {
137
+ if (toolName === "get_ui_components") {
138
+ if (guard.step > 2)
139
+ throw new Error("TOOL_CALL_BLOCKED: cannot restart flow at this step");
140
+ guard.step = 1;
141
+ }
142
+ else if (toolName === "generate_ui_components") {
143
+ if (guard.step !== 1)
144
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
145
+ guard.step = 2;
146
+ }
147
+ else if (toolName === "mark_ui_component_generated") {
148
+ if (guard.step !== 2)
149
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
150
+ resetGuard();
151
+ }
152
+ else {
153
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
154
+ }
155
+ }
156
+ else if (guard.flow === "annotations") {
157
+ if (toolName === "get_annotations") {
158
+ if (guard.step > 2)
159
+ throw new Error("TOOL_CALL_BLOCKED: cannot restart flow at this step");
160
+ guard.step = 1;
161
+ }
162
+ else if (toolName === "mark_annotation_ready") {
163
+ if (guard.step !== 1)
164
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
165
+ guard.step = 2;
166
+ }
167
+ else if (toolName === "mark_annotations_as_live") {
168
+ if (guard.step !== 2)
169
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
170
+ resetGuard();
171
+ }
172
+ else {
173
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
174
+ }
175
+ }
176
+ else if (guard.flow === "ui_improvements") {
177
+ if (toolName === "get_ui_improvements") {
178
+ if (guard.step > 2)
179
+ throw new Error("TOOL_CALL_BLOCKED: cannot restart flow at this step");
180
+ guard.step = 1;
181
+ }
182
+ else if (toolName === "mark_annotation_ready") {
183
+ if (guard.step !== 1)
184
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
185
+ guard.step = 2;
186
+ }
187
+ else if (toolName === "mark_annotations_as_live") {
188
+ if (guard.step !== 2)
189
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
190
+ resetGuard();
191
+ }
192
+ else {
193
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
194
+ }
195
+ }
196
+ else if (guard.flow === "planning") {
197
+ if (toolName !== "planning") {
198
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
199
+ }
200
+ const isSubmit = Array.isArray(args?.results);
201
+ if (!isSubmit) {
202
+ if (guard.step !== 1)
203
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
204
+ guard.step = 1;
205
+ }
206
+ else {
207
+ if (guard.step !== 1)
208
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
209
+ resetGuard();
210
+ }
211
+ }
212
+ else if (guard.flow === "sync") {
213
+ if (toolName === "sync_endpoints") {
214
+ if (guard.step !== 1)
215
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
216
+ guard.step = 2;
217
+ }
218
+ else if (toolName === "get_synced_endpoints") {
219
+ if (guard.step !== 2)
220
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
221
+ resetGuard();
222
+ }
223
+ else {
224
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
225
+ }
226
+ }
227
+ guard.lastTool = toolName;
228
+ guard.lastActivityAt = now;
229
+ }
49
230
  server.setRequestHandler(ListToolsRequestSchema, async () => {
50
231
  return {
51
232
  tools: [
@@ -62,7 +243,7 @@ function createMcpServer(apiKey, personalKey) {
62
243
  },
63
244
  {
64
245
  name: "generate_ui_components",
65
- description: "Generate real UI components into /components (no overwrite). Also generates and uploads a preview, then marks the component as generated.",
246
+ description: "Fetch pending UI component specs and return clear instructions for the LLM to implement a real, functional component. Does not write files, upload previews, or mark components as generated.",
66
247
  inputSchema: {
67
248
  type: "object",
68
249
  properties: {
@@ -99,6 +280,19 @@ function createMcpServer(apiKey, personalKey) {
99
280
  },
100
281
  },
101
282
  },
283
+ {
284
+ name: "get_ui_improvements",
285
+ description: "Fetch pending UI/UX improvement tickets. STRICTLY LIMITED TO VISUAL/CSS CHANGES. Do not add functional logic or modify backend integrations.",
286
+ inputSchema: {
287
+ type: "object",
288
+ properties: {
289
+ limit: { type: "number", description: "Max results.", default: 5 },
290
+ search: { type: "string", description: "Search message/file." },
291
+ key: { type: "string", description: "VG-XXXX key." },
292
+ ids: { type: "string", description: "Internal IDs." }
293
+ },
294
+ },
295
+ },
102
296
  {
103
297
  name: "mark_annotation_ready",
104
298
  description: "Mark as ready/applied. Use internal IDs. IMPORTANT: appliedChanges must be in the project's preferredLanguage (e.g. SPANISH).",
@@ -203,10 +397,12 @@ function createMcpServer(apiKey, personalKey) {
203
397
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
204
398
  const toolName = request.params.name;
205
399
  console.error(`[MCP] Handling tool call: ${toolName}`);
400
+ const argsAny = request.params.arguments;
206
401
  try {
402
+ enforceWorkflow(toolName, argsAny);
207
403
  switch (toolName) {
208
404
  case "get_ui_components": {
209
- const args = request.params.arguments;
405
+ const args = argsAny;
210
406
  const limit = args.limit || 1;
211
407
  const status = args.status || 'pending';
212
408
  const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
@@ -215,6 +411,7 @@ function createMcpServer(apiKey, personalKey) {
215
411
  const response = await fetch(fetchUrl, {
216
412
  headers: {
217
413
  'x-api-key': apiKey,
414
+ 'x-mcp-tool-name': toolName,
218
415
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
219
416
  }
220
417
  });
@@ -226,7 +423,7 @@ function createMcpServer(apiKey, personalKey) {
226
423
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
227
424
  }
228
425
  case "generate_ui_components": {
229
- const args = request.params.arguments;
426
+ const args = argsAny;
230
427
  const limit = Math.min(args.limit || 1, 10);
231
428
  const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
232
429
  fetchUrl.searchParams.append("limit", limit.toString());
@@ -234,6 +431,7 @@ function createMcpServer(apiKey, personalKey) {
234
431
  const response = await fetch(fetchUrl, {
235
432
  headers: {
236
433
  'x-api-key': apiKey,
434
+ 'x-mcp-tool-name': toolName,
237
435
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
238
436
  }
239
437
  });
@@ -256,11 +454,15 @@ function createMcpServer(apiKey, personalKey) {
256
454
  requiredProps,
257
455
  commonProps,
258
456
  figmaUrl: item.figmaUrl,
457
+ htmlContent: item.htmlContent,
458
+ cssContent: item.cssContent,
459
+ sourceType: item.sourceType,
259
460
  constraints: {
260
461
  mustBeFunctional: true,
261
462
  mustSupportRequiredProps: true,
262
463
  mustSupportCommonProps: true,
263
464
  avoidBreakingChanges: true,
465
+ useProvidedHtmlAndCssIfAvailable: true
264
466
  }
265
467
  };
266
468
  results.push({
@@ -285,7 +487,7 @@ function createMcpServer(apiKey, personalKey) {
285
487
  };
286
488
  }
287
489
  case "mark_ui_component_generated": {
288
- const args = request.params.arguments;
490
+ const args = argsAny;
289
491
  if (!args?.id || !args?.code) {
290
492
  throw new Error("id and code are required");
291
493
  }
@@ -294,6 +496,7 @@ function createMcpServer(apiKey, personalKey) {
294
496
  headers: {
295
497
  'Content-Type': 'application/json',
296
498
  'x-api-key': apiKey,
499
+ 'x-mcp-tool-name': toolName,
297
500
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
298
501
  },
299
502
  body: JSON.stringify({ code: args.code, props: args.props })
@@ -306,7 +509,7 @@ function createMcpServer(apiKey, personalKey) {
306
509
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
307
510
  }
308
511
  case "get_annotations": {
309
- const args = request.params.arguments;
512
+ const args = argsAny;
310
513
  const limit = args.limit || 3;
311
514
  // Strictly enforce allowed statuses: pending and bug_fixing
312
515
  const allowedStatuses = ['pending', 'bug_fixing'];
@@ -341,6 +544,7 @@ function createMcpServer(apiKey, personalKey) {
341
544
  const response = await fetch(fetchUrl, {
342
545
  headers: {
343
546
  'x-api-key': apiKey,
547
+ 'x-mcp-tool-name': toolName,
344
548
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
345
549
  }
346
550
  });
@@ -419,7 +623,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
419
623
  };
420
624
  }
421
625
  case "mark_annotation_ready": {
422
- const args = request.params.arguments;
626
+ const args = argsAny;
423
627
  const results = args.results;
424
628
  console.error(`[mark_annotation_ready] Submitting results for ${results?.length} items`);
425
629
  const response = await fetch(`${BACKEND_URL}/api/mcp/annotations/batch-ready`, {
@@ -427,6 +631,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
427
631
  headers: {
428
632
  'Content-Type': 'application/json',
429
633
  'x-api-key': apiKey,
634
+ 'x-mcp-tool-name': toolName,
430
635
  'x-personal-key': personalKey || ''
431
636
  },
432
637
  body: JSON.stringify({
@@ -441,7 +646,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
441
646
  };
442
647
  }
443
648
  case "mark_annotations_as_live": {
444
- const args = request.params.arguments;
649
+ const args = argsAny;
445
650
  const ids = args.ids;
446
651
  console.error(`[mark_annotations_as_live] Marking ${ids?.length} items as live`);
447
652
  const response = await fetch(`${BACKEND_URL}/api/mcp/annotations/mark-as-live`, {
@@ -449,6 +654,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
449
654
  headers: {
450
655
  'Content-Type': 'application/json',
451
656
  'x-api-key': apiKey,
657
+ 'x-mcp-tool-name': toolName,
452
658
  'x-personal-key': personalKey || ''
453
659
  },
454
660
  body: JSON.stringify({ ids })
@@ -461,12 +667,12 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
461
667
  };
462
668
  }
463
669
  case "planning": {
464
- const args = request.params.arguments;
670
+ const args = argsAny;
465
671
  const url = args.results ? `${BACKEND_URL}/api/mcp/annotations/batch-planning` : `${BACKEND_URL}/api/mcp/backlog`;
466
672
  const method = args.results ? 'PATCH' : 'GET';
467
673
  const response = await fetch(url, {
468
674
  method,
469
- headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, ...(personalKey ? { 'x-personal-key': personalKey } : {}) },
675
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) },
470
676
  body: args.results ? JSON.stringify({ results: args.results, force: args.force }) : undefined
471
677
  });
472
678
  if (!response.ok)
@@ -477,11 +683,39 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
477
683
  : (data.preferredLanguage === 'en' ? "\n*** [INSTRUCTION: Provide all comments and analysis in English.] ***\n\n\n\n" : "");
478
684
  return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
479
685
  }
686
+ case "get_ui_improvements": {
687
+ const args = argsAny;
688
+ const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/ui-improvements`);
689
+ if (args.limit)
690
+ fetchUrl.searchParams.append("limit", args.limit.toString());
691
+ if (args.search)
692
+ fetchUrl.searchParams.append("search", args.search);
693
+ if (args.key)
694
+ fetchUrl.searchParams.append("key", args.key);
695
+ if (args.ids)
696
+ fetchUrl.searchParams.append("ids", args.ids);
697
+ const response = await fetch(fetchUrl, {
698
+ headers: {
699
+ 'x-api-key': apiKey,
700
+ 'x-mcp-tool-name': toolName,
701
+ ...(personalKey ? { 'x-personal-key': personalKey } : {})
702
+ }
703
+ });
704
+ if (!response.ok) {
705
+ const errorBody = await response.text();
706
+ throw new Error(`Backend responded with ${response.status}: ${errorBody}`);
707
+ }
708
+ const data = (await response.json());
709
+ const langHint = data.preferredLanguage === 'es'
710
+ ? "\n*** [INSTRUCTION: Project is in SPANISH. Provide all CSS/Visual changes in SPANISH comments if requested.] ***\n\n\n\n"
711
+ : "";
712
+ return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
713
+ }
480
714
  case "sync_endpoints": {
481
- const args = request.params.arguments;
715
+ const args = argsAny;
482
716
  const response = await fetch(`${BACKEND_URL}/api/mcp/sync-endpoints`, {
483
717
  method: 'POST',
484
- headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, ...(personalKey ? { 'x-personal-key': personalKey } : {}) },
718
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) },
485
719
  body: JSON.stringify(args)
486
720
  });
487
721
  if (!response.ok)
@@ -491,7 +725,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
491
725
  }
492
726
  case "get_synced_endpoints": {
493
727
  const response = await fetch(`${BACKEND_URL}/api/projects/endpoints`, {
494
- headers: { 'x-api-key': apiKey, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
728
+ headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
495
729
  });
496
730
  if (!response.ok)
497
731
  throw new Error(`Backend responded with ${response.status}`);
@@ -500,7 +734,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
500
734
  }
501
735
  case "get_ai_resolved_tickets": {
502
736
  const response = await fetch(`${BACKEND_URL}/api/mcp/resolved-tickets`, {
503
- headers: { 'x-api-key': apiKey, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
737
+ headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
504
738
  });
505
739
  if (!response.ok)
506
740
  throw new Error(`Backend responded with ${response.status}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viewgate-mcp",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "viewgate-mcp": "./dist/index.js"