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.
- package/dist/index.js +198 -13
- 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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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}`);
|