viewgate-mcp 1.0.44 → 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 +203 -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: [
@@ -216,10 +397,12 @@ function createMcpServer(apiKey, personalKey) {
216
397
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
217
398
  const toolName = request.params.name;
218
399
  console.error(`[MCP] Handling tool call: ${toolName}`);
400
+ const argsAny = request.params.arguments;
219
401
  try {
402
+ enforceWorkflow(toolName, argsAny);
220
403
  switch (toolName) {
221
404
  case "get_ui_components": {
222
- const args = request.params.arguments;
405
+ const args = argsAny;
223
406
  const limit = args.limit || 1;
224
407
  const status = args.status || 'pending';
225
408
  const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
@@ -228,6 +411,7 @@ function createMcpServer(apiKey, personalKey) {
228
411
  const response = await fetch(fetchUrl, {
229
412
  headers: {
230
413
  'x-api-key': apiKey,
414
+ 'x-mcp-tool-name': toolName,
231
415
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
232
416
  }
233
417
  });
@@ -239,7 +423,7 @@ function createMcpServer(apiKey, personalKey) {
239
423
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
240
424
  }
241
425
  case "generate_ui_components": {
242
- const args = request.params.arguments;
426
+ const args = argsAny;
243
427
  const limit = Math.min(args.limit || 1, 10);
244
428
  const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
245
429
  fetchUrl.searchParams.append("limit", limit.toString());
@@ -247,6 +431,7 @@ function createMcpServer(apiKey, personalKey) {
247
431
  const response = await fetch(fetchUrl, {
248
432
  headers: {
249
433
  'x-api-key': apiKey,
434
+ 'x-mcp-tool-name': toolName,
250
435
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
251
436
  }
252
437
  });
@@ -302,7 +487,7 @@ function createMcpServer(apiKey, personalKey) {
302
487
  };
303
488
  }
304
489
  case "mark_ui_component_generated": {
305
- const args = request.params.arguments;
490
+ const args = argsAny;
306
491
  if (!args?.id || !args?.code) {
307
492
  throw new Error("id and code are required");
308
493
  }
@@ -311,6 +496,7 @@ function createMcpServer(apiKey, personalKey) {
311
496
  headers: {
312
497
  'Content-Type': 'application/json',
313
498
  'x-api-key': apiKey,
499
+ 'x-mcp-tool-name': toolName,
314
500
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
315
501
  },
316
502
  body: JSON.stringify({ code: args.code, props: args.props })
@@ -323,7 +509,7 @@ function createMcpServer(apiKey, personalKey) {
323
509
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
324
510
  }
325
511
  case "get_annotations": {
326
- const args = request.params.arguments;
512
+ const args = argsAny;
327
513
  const limit = args.limit || 3;
328
514
  // Strictly enforce allowed statuses: pending and bug_fixing
329
515
  const allowedStatuses = ['pending', 'bug_fixing'];
@@ -358,6 +544,7 @@ function createMcpServer(apiKey, personalKey) {
358
544
  const response = await fetch(fetchUrl, {
359
545
  headers: {
360
546
  'x-api-key': apiKey,
547
+ 'x-mcp-tool-name': toolName,
361
548
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
362
549
  }
363
550
  });
@@ -436,7 +623,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
436
623
  };
437
624
  }
438
625
  case "mark_annotation_ready": {
439
- const args = request.params.arguments;
626
+ const args = argsAny;
440
627
  const results = args.results;
441
628
  console.error(`[mark_annotation_ready] Submitting results for ${results?.length} items`);
442
629
  const response = await fetch(`${BACKEND_URL}/api/mcp/annotations/batch-ready`, {
@@ -444,6 +631,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
444
631
  headers: {
445
632
  'Content-Type': 'application/json',
446
633
  'x-api-key': apiKey,
634
+ 'x-mcp-tool-name': toolName,
447
635
  'x-personal-key': personalKey || ''
448
636
  },
449
637
  body: JSON.stringify({
@@ -458,7 +646,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
458
646
  };
459
647
  }
460
648
  case "mark_annotations_as_live": {
461
- const args = request.params.arguments;
649
+ const args = argsAny;
462
650
  const ids = args.ids;
463
651
  console.error(`[mark_annotations_as_live] Marking ${ids?.length} items as live`);
464
652
  const response = await fetch(`${BACKEND_URL}/api/mcp/annotations/mark-as-live`, {
@@ -466,6 +654,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
466
654
  headers: {
467
655
  'Content-Type': 'application/json',
468
656
  'x-api-key': apiKey,
657
+ 'x-mcp-tool-name': toolName,
469
658
  'x-personal-key': personalKey || ''
470
659
  },
471
660
  body: JSON.stringify({ ids })
@@ -478,12 +667,12 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
478
667
  };
479
668
  }
480
669
  case "planning": {
481
- const args = request.params.arguments;
670
+ const args = argsAny;
482
671
  const url = args.results ? `${BACKEND_URL}/api/mcp/annotations/batch-planning` : `${BACKEND_URL}/api/mcp/backlog`;
483
672
  const method = args.results ? 'PATCH' : 'GET';
484
673
  const response = await fetch(url, {
485
674
  method,
486
- 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 } : {}) },
487
676
  body: args.results ? JSON.stringify({ results: args.results, force: args.force }) : undefined
488
677
  });
489
678
  if (!response.ok)
@@ -495,7 +684,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
495
684
  return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
496
685
  }
497
686
  case "get_ui_improvements": {
498
- const args = request.params.arguments;
687
+ const args = argsAny;
499
688
  const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/ui-improvements`);
500
689
  if (args.limit)
501
690
  fetchUrl.searchParams.append("limit", args.limit.toString());
@@ -508,6 +697,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
508
697
  const response = await fetch(fetchUrl, {
509
698
  headers: {
510
699
  'x-api-key': apiKey,
700
+ 'x-mcp-tool-name': toolName,
511
701
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
512
702
  }
513
703
  });
@@ -522,10 +712,10 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
522
712
  return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
523
713
  }
524
714
  case "sync_endpoints": {
525
- const args = request.params.arguments;
715
+ const args = argsAny;
526
716
  const response = await fetch(`${BACKEND_URL}/api/mcp/sync-endpoints`, {
527
717
  method: 'POST',
528
- 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 } : {}) },
529
719
  body: JSON.stringify(args)
530
720
  });
531
721
  if (!response.ok)
@@ -535,7 +725,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
535
725
  }
536
726
  case "get_synced_endpoints": {
537
727
  const response = await fetch(`${BACKEND_URL}/api/projects/endpoints`, {
538
- 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 } : {}) }
539
729
  });
540
730
  if (!response.ok)
541
731
  throw new Error(`Backend responded with ${response.status}`);
@@ -544,7 +734,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
544
734
  }
545
735
  case "get_ai_resolved_tickets": {
546
736
  const response = await fetch(`${BACKEND_URL}/api/mcp/resolved-tickets`, {
547
- 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 } : {}) }
548
738
  });
549
739
  if (!response.ok)
550
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.44",
3
+ "version": "1.0.45",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "viewgate-mcp": "./dist/index.js"