viewgate-mcp 1.0.44 → 1.0.46

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 +198 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -46,6 +46,182 @@ 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
+ return "sync";
79
+ case "get_synced_endpoints":
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
+ // Neutral tools (idle flow) are always allowed and don't interrupt active flows
93
+ if (desiredFlow === "idle") {
94
+ guard.lastTool = toolName;
95
+ guard.lastActivityAt = now;
96
+ return;
97
+ }
98
+ if (guard.flow === "idle" || desiredFlow !== guard.flow) {
99
+ // Implicit flow reset if we are starting a new flow with a "start tool"
100
+ switch (toolName) {
101
+ case "get_ui_components":
102
+ guard.flow = "ui_components";
103
+ guard.step = 1;
104
+ break;
105
+ case "get_annotations":
106
+ guard.flow = "annotations";
107
+ guard.step = 1;
108
+ break;
109
+ case "get_ui_improvements":
110
+ guard.flow = "ui_improvements";
111
+ guard.step = 1;
112
+ break;
113
+ case "planning":
114
+ if (args?.results) {
115
+ throw new Error("TOOL_CALL_BLOCKED: planning submit requires prior fetch");
116
+ }
117
+ guard.flow = "planning";
118
+ guard.step = 1;
119
+ break;
120
+ case "sync_endpoints":
121
+ guard.flow = "sync";
122
+ guard.step = 1;
123
+ break;
124
+ default:
125
+ if (guard.flow === "idle") {
126
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in idle");
127
+ }
128
+ else {
129
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
130
+ }
131
+ }
132
+ guard.startedAt = now;
133
+ guard.lastTool = toolName;
134
+ guard.lastActivityAt = now;
135
+ return;
136
+ }
137
+ if (guard.flow === "ui_components") {
138
+ if (toolName === "get_ui_components") {
139
+ if (guard.step > 2)
140
+ throw new Error("TOOL_CALL_BLOCKED: cannot restart flow at this step");
141
+ guard.step = 1;
142
+ }
143
+ else if (toolName === "generate_ui_components") {
144
+ if (guard.step !== 1)
145
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
146
+ guard.step = 2;
147
+ }
148
+ else if (toolName === "mark_ui_component_generated") {
149
+ if (guard.step !== 2)
150
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
151
+ resetGuard();
152
+ }
153
+ else {
154
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
155
+ }
156
+ }
157
+ else if (guard.flow === "annotations") {
158
+ if (toolName === "get_annotations") {
159
+ // Allow re-fetching at any time during the flow
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
+ // Stop here and reset flow as per USER_REQUEST (MCP only reaches 'applied')
166
+ resetGuard();
167
+ }
168
+ else if (toolName === "mark_annotations_as_live") {
169
+ // Optional step if the model decides to use it, but no longer part of the required chain
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
+ guard.step = 1;
179
+ }
180
+ else if (toolName === "mark_annotation_ready") {
181
+ if (guard.step !== 1)
182
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
183
+ resetGuard();
184
+ }
185
+ else if (toolName === "mark_annotations_as_live") {
186
+ resetGuard();
187
+ }
188
+ else {
189
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
190
+ }
191
+ }
192
+ else if (guard.flow === "planning") {
193
+ if (toolName !== "planning") {
194
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
195
+ }
196
+ const isSubmit = Array.isArray(args?.results);
197
+ if (!isSubmit) {
198
+ if (guard.step !== 1)
199
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
200
+ guard.step = 1;
201
+ }
202
+ else {
203
+ if (guard.step !== 1)
204
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
205
+ resetGuard();
206
+ }
207
+ }
208
+ else if (guard.flow === "sync") {
209
+ if (toolName === "sync_endpoints") {
210
+ if (guard.step !== 1)
211
+ throw new Error("TOOL_CALL_BLOCKED: unexpected step");
212
+ guard.step = 2;
213
+ }
214
+ else if (toolName === "get_synced_endpoints") {
215
+ // Actually get_synced_endpoints is now neutral, but if called as part of flow, reset.
216
+ resetGuard();
217
+ }
218
+ else {
219
+ throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
220
+ }
221
+ }
222
+ guard.lastTool = toolName;
223
+ guard.lastActivityAt = now;
224
+ }
49
225
  server.setRequestHandler(ListToolsRequestSchema, async () => {
50
226
  return {
51
227
  tools: [
@@ -216,10 +392,12 @@ function createMcpServer(apiKey, personalKey) {
216
392
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
217
393
  const toolName = request.params.name;
218
394
  console.error(`[MCP] Handling tool call: ${toolName}`);
395
+ const argsAny = request.params.arguments;
219
396
  try {
397
+ enforceWorkflow(toolName, argsAny);
220
398
  switch (toolName) {
221
399
  case "get_ui_components": {
222
- const args = request.params.arguments;
400
+ const args = argsAny;
223
401
  const limit = args.limit || 1;
224
402
  const status = args.status || 'pending';
225
403
  const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
@@ -228,6 +406,7 @@ function createMcpServer(apiKey, personalKey) {
228
406
  const response = await fetch(fetchUrl, {
229
407
  headers: {
230
408
  'x-api-key': apiKey,
409
+ 'x-mcp-tool-name': toolName,
231
410
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
232
411
  }
233
412
  });
@@ -239,7 +418,7 @@ function createMcpServer(apiKey, personalKey) {
239
418
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
240
419
  }
241
420
  case "generate_ui_components": {
242
- const args = request.params.arguments;
421
+ const args = argsAny;
243
422
  const limit = Math.min(args.limit || 1, 10);
244
423
  const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
245
424
  fetchUrl.searchParams.append("limit", limit.toString());
@@ -247,6 +426,7 @@ function createMcpServer(apiKey, personalKey) {
247
426
  const response = await fetch(fetchUrl, {
248
427
  headers: {
249
428
  'x-api-key': apiKey,
429
+ 'x-mcp-tool-name': toolName,
250
430
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
251
431
  }
252
432
  });
@@ -302,7 +482,7 @@ function createMcpServer(apiKey, personalKey) {
302
482
  };
303
483
  }
304
484
  case "mark_ui_component_generated": {
305
- const args = request.params.arguments;
485
+ const args = argsAny;
306
486
  if (!args?.id || !args?.code) {
307
487
  throw new Error("id and code are required");
308
488
  }
@@ -311,6 +491,7 @@ function createMcpServer(apiKey, personalKey) {
311
491
  headers: {
312
492
  'Content-Type': 'application/json',
313
493
  'x-api-key': apiKey,
494
+ 'x-mcp-tool-name': toolName,
314
495
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
315
496
  },
316
497
  body: JSON.stringify({ code: args.code, props: args.props })
@@ -323,7 +504,7 @@ function createMcpServer(apiKey, personalKey) {
323
504
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
324
505
  }
325
506
  case "get_annotations": {
326
- const args = request.params.arguments;
507
+ const args = argsAny;
327
508
  const limit = args.limit || 3;
328
509
  // Strictly enforce allowed statuses: pending and bug_fixing
329
510
  const allowedStatuses = ['pending', 'bug_fixing'];
@@ -358,6 +539,7 @@ function createMcpServer(apiKey, personalKey) {
358
539
  const response = await fetch(fetchUrl, {
359
540
  headers: {
360
541
  'x-api-key': apiKey,
542
+ 'x-mcp-tool-name': toolName,
361
543
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
362
544
  }
363
545
  });
@@ -436,7 +618,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
436
618
  };
437
619
  }
438
620
  case "mark_annotation_ready": {
439
- const args = request.params.arguments;
621
+ const args = argsAny;
440
622
  const results = args.results;
441
623
  console.error(`[mark_annotation_ready] Submitting results for ${results?.length} items`);
442
624
  const response = await fetch(`${BACKEND_URL}/api/mcp/annotations/batch-ready`, {
@@ -444,6 +626,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
444
626
  headers: {
445
627
  'Content-Type': 'application/json',
446
628
  'x-api-key': apiKey,
629
+ 'x-mcp-tool-name': toolName,
447
630
  'x-personal-key': personalKey || ''
448
631
  },
449
632
  body: JSON.stringify({
@@ -458,7 +641,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
458
641
  };
459
642
  }
460
643
  case "mark_annotations_as_live": {
461
- const args = request.params.arguments;
644
+ const args = argsAny;
462
645
  const ids = args.ids;
463
646
  console.error(`[mark_annotations_as_live] Marking ${ids?.length} items as live`);
464
647
  const response = await fetch(`${BACKEND_URL}/api/mcp/annotations/mark-as-live`, {
@@ -466,6 +649,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
466
649
  headers: {
467
650
  'Content-Type': 'application/json',
468
651
  'x-api-key': apiKey,
652
+ 'x-mcp-tool-name': toolName,
469
653
  'x-personal-key': personalKey || ''
470
654
  },
471
655
  body: JSON.stringify({ ids })
@@ -478,12 +662,12 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
478
662
  };
479
663
  }
480
664
  case "planning": {
481
- const args = request.params.arguments;
665
+ const args = argsAny;
482
666
  const url = args.results ? `${BACKEND_URL}/api/mcp/annotations/batch-planning` : `${BACKEND_URL}/api/mcp/backlog`;
483
667
  const method = args.results ? 'PATCH' : 'GET';
484
668
  const response = await fetch(url, {
485
669
  method,
486
- headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, ...(personalKey ? { 'x-personal-key': personalKey } : {}) },
670
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) },
487
671
  body: args.results ? JSON.stringify({ results: args.results, force: args.force }) : undefined
488
672
  });
489
673
  if (!response.ok)
@@ -495,7 +679,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
495
679
  return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
496
680
  }
497
681
  case "get_ui_improvements": {
498
- const args = request.params.arguments;
682
+ const args = argsAny;
499
683
  const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/ui-improvements`);
500
684
  if (args.limit)
501
685
  fetchUrl.searchParams.append("limit", args.limit.toString());
@@ -508,6 +692,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
508
692
  const response = await fetch(fetchUrl, {
509
693
  headers: {
510
694
  'x-api-key': apiKey,
695
+ 'x-mcp-tool-name': toolName,
511
696
  ...(personalKey ? { 'x-personal-key': personalKey } : {})
512
697
  }
513
698
  });
@@ -522,10 +707,10 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
522
707
  return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
523
708
  }
524
709
  case "sync_endpoints": {
525
- const args = request.params.arguments;
710
+ const args = argsAny;
526
711
  const response = await fetch(`${BACKEND_URL}/api/mcp/sync-endpoints`, {
527
712
  method: 'POST',
528
- headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, ...(personalKey ? { 'x-personal-key': personalKey } : {}) },
713
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) },
529
714
  body: JSON.stringify(args)
530
715
  });
531
716
  if (!response.ok)
@@ -535,7 +720,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
535
720
  }
536
721
  case "get_synced_endpoints": {
537
722
  const response = await fetch(`${BACKEND_URL}/api/projects/endpoints`, {
538
- headers: { 'x-api-key': apiKey, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
723
+ headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
539
724
  });
540
725
  if (!response.ok)
541
726
  throw new Error(`Backend responded with ${response.status}`);
@@ -544,7 +729,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
544
729
  }
545
730
  case "get_ai_resolved_tickets": {
546
731
  const response = await fetch(`${BACKEND_URL}/api/mcp/resolved-tickets`, {
547
- headers: { 'x-api-key': apiKey, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
732
+ headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
548
733
  });
549
734
  if (!response.ok)
550
735
  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.46",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "viewgate-mcp": "./dist/index.js"