viewgate-mcp 1.0.37 → 1.0.39

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 +246 -36
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -23,6 +23,8 @@ import dotenv from "dotenv";
23
23
  import path from "path";
24
24
  import os from "os";
25
25
  import { fileURLToPath } from "url";
26
+ import fs from "fs";
27
+ import fsp from "fs/promises";
26
28
  const __filename = fileURLToPath(import.meta.url);
27
29
  const __dirname = path.dirname(__filename);
28
30
  dotenv.config({ path: path.join(__dirname, "..", ".env") });
@@ -49,6 +51,28 @@ function createMcpServer(apiKey, personalKey) {
49
51
  server.setRequestHandler(ListToolsRequestSchema, async () => {
50
52
  return {
51
53
  tools: [
54
+ {
55
+ name: "get_ui_components",
56
+ description: "Fetch UI components created by UX/UI (progressive). Returns ONLY the pending components requested and their required props.",
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {
60
+ limit: { type: "number", description: "Max results.", default: 1 },
61
+ status: { type: "string", description: "Component status filter (pending,generated).", default: "pending" },
62
+ },
63
+ },
64
+ },
65
+ {
66
+ name: "generate_ui_components",
67
+ description: "Generate real UI components into /components (no overwrite). Also generates and uploads a preview, then marks the component as generated.",
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ limit: { type: "number", description: "How many pending components to generate.", default: 1 },
72
+ framework: { type: "string", description: "Target: react or react+vite (both output React components).", default: "react" },
73
+ },
74
+ },
75
+ },
52
76
  {
53
77
  name: "get_annotations",
54
78
  description: "Fetch feedback. Keys ('VG-XXXX') or IDs. Workflow: 1. Fetch, 2. Fix, 3. Mark Ready.",
@@ -66,7 +90,7 @@ function createMcpServer(apiKey, personalKey) {
66
90
  },
67
91
  {
68
92
  name: "mark_annotation_ready",
69
- description: "Mark as ready/applied. Use internal IDs.",
93
+ description: "Mark as ready/applied. Use internal IDs. IMPORTANT: appliedChanges must be in the project's preferredLanguage (e.g. SPANISH).",
70
94
  inputSchema: {
71
95
  type: "object",
72
96
  properties: {
@@ -76,7 +100,7 @@ function createMcpServer(apiKey, personalKey) {
76
100
  type: "object",
77
101
  properties: {
78
102
  id: { type: "string", description: "Internal ID." },
79
- appliedChanges: { type: "string", description: "Summary." }
103
+ appliedChanges: { type: "string", description: "Summary of changes. (IMPORTANT: Must be in SPANISH if project is ES)" }
80
104
  },
81
105
  required: ["id", "appliedChanges"]
82
106
  }
@@ -102,7 +126,7 @@ function createMcpServer(apiKey, personalKey) {
102
126
  },
103
127
  {
104
128
  name: "planning",
105
- description: "Planning tool for backlog tickets. Fetch tickets or submit analysis.",
129
+ description: "Planning tool for backlog tickets. Fetch tickets or submit analysis. IMPORTANT: aiAnalysis must be in the project's preferredLanguage (e.g. SPANISH).",
106
130
  inputSchema: {
107
131
  type: "object",
108
132
  properties: {
@@ -117,7 +141,7 @@ function createMcpServer(apiKey, personalKey) {
117
141
  impacto: { type: "number", minimum: 1, maximum: 3 },
118
142
  riesgo: { type: "number", minimum: 1, maximum: 3 },
119
143
  tipo: { type: "string", enum: ["AI-friendly", "AI-assisted", "Human-critical"] },
120
- aiAnalysis: { type: "string" }
144
+ aiAnalysis: { type: "string", description: "Detailed analysis. (IMPORTANT: Must be in SPANISH if project is ES)" }
121
145
  },
122
146
  required: ["id", "complejidad", "incertidumbre", "impacto", "riesgo", "tipo", "aiAnalysis"]
123
147
  }
@@ -165,11 +189,213 @@ function createMcpServer(apiKey, personalKey) {
165
189
  ],
166
190
  };
167
191
  });
192
+ const toPascalCase = (input) => {
193
+ return (input || '')
194
+ .replace(/[^a-zA-Z0-9 ]+/g, ' ')
195
+ .split(' ')
196
+ .map((w) => w.trim())
197
+ .filter(Boolean)
198
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
199
+ .join('');
200
+ };
201
+ const normalizePropTokens = (raw) => {
202
+ const line = (raw || '').trim();
203
+ if (!line)
204
+ return [];
205
+ // Split "onClick / onChange / onFocus / onBlur" style
206
+ const parts = line.split('/').map((s) => s.trim()).filter(Boolean);
207
+ return parts.flatMap((p) => {
208
+ // Take part before spaces or parentheses for "variant (primary...)" patterns
209
+ const base = p.split('(')[0].trim();
210
+ const token = base.split(' ')[0].trim();
211
+ return token ? [token] : [];
212
+ });
213
+ };
214
+ const buildPropModel = (commonProps, requiredProps) => {
215
+ const out = {
216
+ hasChildren: false,
217
+ hasAs: false,
218
+ hasDataIndex: false,
219
+ hasAriaIndex: false,
220
+ common: [],
221
+ required: [],
222
+ };
223
+ const add = (arr, target) => {
224
+ for (const raw of arr) {
225
+ for (const token of normalizePropTokens(raw)) {
226
+ if (token === 'children')
227
+ out.hasChildren = true;
228
+ if (token === 'as')
229
+ out.hasAs = true;
230
+ if (token.startsWith('data-'))
231
+ out.hasDataIndex = true;
232
+ if (token.startsWith('aria-'))
233
+ out.hasAriaIndex = true;
234
+ if (token === 'ref')
235
+ continue; // handled via forwardRef if needed later
236
+ if (token === 'data-*' || token === 'aria-*')
237
+ continue;
238
+ // skip wildcard tokens, handled by index signatures
239
+ if (token.endsWith('*'))
240
+ continue;
241
+ target.push(token);
242
+ }
243
+ }
244
+ };
245
+ add(commonProps || [], out.common);
246
+ add(requiredProps || [], out.required);
247
+ const uniq = (xs) => [...new Set(xs)].filter(Boolean);
248
+ out.common = uniq(out.common);
249
+ out.required = uniq(out.required);
250
+ return out;
251
+ };
252
+ const writeComponentFile = async (componentType, model) => {
253
+ const cwd = process.cwd();
254
+ const componentsDir = path.join(cwd, 'components');
255
+ await fsp.mkdir(componentsDir, { recursive: true });
256
+ const baseName = toPascalCase(componentType) || 'Component';
257
+ const hasTs = fs.existsSync(path.join(cwd, 'tsconfig.json'));
258
+ const ext = hasTs ? 'tsx' : 'jsx';
259
+ const fileBase = `${baseName}.${ext}`;
260
+ let filePath = path.join(componentsDir, fileBase);
261
+ if (fs.existsSync(filePath)) {
262
+ // never overwrite existing
263
+ let i = 2;
264
+ while (fs.existsSync(path.join(componentsDir, `${baseName}.${i}.${ext}`)))
265
+ i++;
266
+ filePath = path.join(componentsDir, `${baseName}.${i}.${ext}`);
267
+ }
268
+ const isTs = ext === 'tsx';
269
+ const commonOptional = model.common.map((p) => `${p}${isTs ? '?: any' : ''}`);
270
+ const required = model.required.map((p) => `${p}${isTs ? ': any' : ''}`);
271
+ const ariaIndex = model.hasAriaIndex ? (isTs ? `\n [key: \`aria-\${string}\`]: any;` : '') : '';
272
+ const dataIndex = model.hasDataIndex ? (isTs ? `\n [key: \`data-\${string}\`]: any;` : '') : '';
273
+ const propsBlock = isTs
274
+ ? `export type ${baseName}Props = {\n${[
275
+ model.hasAs ? ` as?: React.ElementType;` : null,
276
+ model.hasChildren ? ` children?: React.ReactNode;` : null,
277
+ ...required.map((l) => ` ${l};`),
278
+ ...commonOptional.map((l) => ` ${l};`),
279
+ ].filter(Boolean).join('\n')}${ariaIndex}${dataIndex}\n};`
280
+ : '';
281
+ const destructure = [
282
+ model.hasAs ? 'as: Comp = "div"' : null,
283
+ model.hasChildren ? 'children' : null,
284
+ ...model.required,
285
+ ...model.common,
286
+ '...rest'
287
+ ].filter(Boolean).join(', ');
288
+ const jsxTag = model.hasAs ? '<Comp' : '<div';
289
+ const jsxClose = model.hasAs ? '</Comp>' : '</div>';
290
+ const content = isTs
291
+ ? `import React from 'react';\n\n${propsBlock}\n\nexport function ${baseName}({ ${destructure} }: ${baseName}Props) {\n return (\n ${jsxTag} {...rest}>\n {children}\n ${jsxClose}\n );\n}\n`
292
+ : `import React from 'react';\n\nexport function ${baseName}(props) {\n const { ${destructure} } = props || {};\n return (\n ${jsxTag} {...rest}>\n {children}\n ${jsxClose}\n );\n}\n`;
293
+ await fsp.writeFile(filePath, content, 'utf8');
294
+ return { filePath, fileName: path.basename(filePath) };
295
+ };
296
+ const makePreviewSvg = (title, props) => {
297
+ const safeTitle = (title || '').replace(/[<>]/g, '');
298
+ const list = (props || []).slice(0, 14).map((p) => p.replace(/[<>]/g, '')).join(' • ');
299
+ const text = `${safeTitle}${list ? ' — ' + list : ''}`;
300
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="420">\n <defs>\n <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">\n <stop offset="0" stop-color="#0f172a"/>\n <stop offset="1" stop-color="#111827"/>\n </linearGradient>\n </defs>\n <rect width="1200" height="420" rx="32" fill="url(#g)"/>\n <rect x="40" y="40" width="1120" height="340" rx="28" fill="#0b1220" stroke="rgba(255,255,255,0.08)"/>\n <text x="80" y="150" font-family="Inter,ui-sans-serif,system-ui" font-size="46" font-weight="800" fill="#e2e8f0">${safeTitle}</text>\n <text x="80" y="220" font-family="ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace" font-size="22" font-weight="700" fill="#94a3b8">Preview (fast)</text>\n <text x="80" y="280" font-family="Inter,ui-sans-serif,system-ui" font-size="18" font-weight="600" fill="#64748b">${text}</text>\n</svg>`;
301
+ };
168
302
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
169
303
  const toolName = request.params.name;
170
304
  console.error(`[MCP] Handling tool call: ${toolName}`);
171
305
  try {
172
306
  switch (toolName) {
307
+ case "get_ui_components": {
308
+ const args = request.params.arguments;
309
+ const limit = args.limit || 1;
310
+ const status = args.status || 'pending';
311
+ const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
312
+ fetchUrl.searchParams.append("limit", limit.toString());
313
+ fetchUrl.searchParams.append("status", status);
314
+ const response = await fetch(fetchUrl, {
315
+ headers: {
316
+ 'x-api-key': apiKey,
317
+ ...(personalKey ? { 'x-personal-key': personalKey } : {})
318
+ }
319
+ });
320
+ if (!response.ok) {
321
+ const errorBody = await response.text();
322
+ throw new Error(`Backend responded with ${response.status}: ${errorBody}`);
323
+ }
324
+ const data = await response.json();
325
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
326
+ }
327
+ case "generate_ui_components": {
328
+ const args = request.params.arguments;
329
+ const limit = Math.min(args.limit || 1, 10);
330
+ const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
331
+ fetchUrl.searchParams.append("limit", limit.toString());
332
+ fetchUrl.searchParams.append("status", "pending");
333
+ const response = await fetch(fetchUrl, {
334
+ headers: {
335
+ 'x-api-key': apiKey,
336
+ ...(personalKey ? { 'x-personal-key': personalKey } : {})
337
+ }
338
+ });
339
+ if (!response.ok) {
340
+ const errorBody = await response.text();
341
+ throw new Error(`Backend responded with ${response.status}: ${errorBody}`);
342
+ }
343
+ const payload = (await response.json());
344
+ const items = payload?.data || [];
345
+ if (!Array.isArray(items) || items.length === 0) {
346
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, generated: 0, message: "No pending UI components." }, null, 2) }] };
347
+ }
348
+ const results = [];
349
+ for (const item of items) {
350
+ const componentType = item.componentType;
351
+ const requiredProps = (item.requiredProps || []);
352
+ const commonProps = (item.commonProps || []);
353
+ const model = buildPropModel(commonProps, requiredProps);
354
+ const fileInfo = await writeComponentFile(componentType, model);
355
+ // Fast preview generation (SVG placeholder) + upload
356
+ const svg = makePreviewSvg(componentType, requiredProps);
357
+ const svg64 = Buffer.from(svg, 'utf8').toString('base64');
358
+ const imageDataUrl = `data:image/svg+xml;base64,${svg64}`;
359
+ const uploadResp = await fetch(`${BACKEND_URL}/api/screenshots/upload`, {
360
+ method: 'POST',
361
+ headers: {
362
+ 'Content-Type': 'application/json',
363
+ 'x-api-key': apiKey,
364
+ ...(personalKey ? { 'x-personal-key': personalKey } : {})
365
+ },
366
+ body: JSON.stringify({ image: imageDataUrl })
367
+ });
368
+ if (!uploadResp.ok) {
369
+ const errorBody = await uploadResp.text();
370
+ throw new Error(`Preview upload failed (${uploadResp.status}): ${errorBody}`);
371
+ }
372
+ const uploaded = (await uploadResp.json());
373
+ const previewUrl = uploaded?.url || null;
374
+ const markResp = await fetch(`${BACKEND_URL}/api/mcp/components/${item._id}/generated`, {
375
+ method: 'PATCH',
376
+ headers: {
377
+ 'Content-Type': 'application/json',
378
+ 'x-api-key': apiKey,
379
+ ...(personalKey ? { 'x-personal-key': personalKey } : {})
380
+ },
381
+ body: JSON.stringify({ previewImage: previewUrl })
382
+ });
383
+ if (!markResp.ok) {
384
+ const errorBody = await markResp.text();
385
+ throw new Error(`Mark generated failed (${markResp.status}): ${errorBody}`);
386
+ }
387
+ const marked = (await markResp.json());
388
+ results.push({
389
+ componentType,
390
+ figmaUrl: item.figmaUrl,
391
+ requiredProps,
392
+ written: fileInfo,
393
+ previewImage: previewUrl,
394
+ db: marked?.data?._id || item._id
395
+ });
396
+ }
397
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, generated: results.length, results }, null, 2) }] };
398
+ }
173
399
  case "get_annotations": {
174
400
  const args = request.params.arguments;
175
401
  const limit = args.limit || 3;
@@ -260,41 +486,25 @@ function createMcpServer(apiKey, personalKey) {
260
486
  const annotationsWithTips = sortedAnnotations.map((ann) => {
261
487
  const file = ann.reference?.source?.split(':')[0];
262
488
  const line = ann.reference?.source?.split(':')[1];
263
- const pendingCorrection = Array.isArray(ann.corrections)
264
- ? ann.corrections.find((c) => c.status === 'pending')
265
- : (typeof ann.corrections === 'string' ? { text: ann.corrections } : null);
266
489
  const figmaLinks = Array.isArray(ann.figmaReference)
267
490
  ? ann.figmaReference.filter((fr) => fr.link).map((fr) => `[${fr.name || 'Design'}](${fr.link})`).join(', ')
268
491
  : (ann.figmaReference && typeof ann.figmaReference === 'string' ? ann.figmaReference : null);
269
492
  return {
270
- id: ann._id,
271
- key: ann.key,
272
- priority: ann.priority,
273
- status: ann.status,
274
- message: ann.message,
275
- corrections: ann.corrections,
276
- source: ann.reference?.source,
277
- filePath: ann.filePath || file,
278
- line: ann.line || (line ? parseInt(line) : undefined),
279
- vgId: ann.reference?.vgId,
280
- outerHtml: ann.reference?.outerHtml,
281
- focusAreas: ann.focusAreas || [],
282
- componentName: ann.componentName,
283
- figmaReference: ann.figmaReference,
284
- backendEndpoints: ann.backendEndpoints || [],
285
- _ia_fix_instruction: `[AUTONOMOUS_MODE: TRUE]
286
- Target: ${file} (data-vg-id="${ann.reference?.vgId}")
287
- Context: ${ann.reference?.parentContext?.slice(0, 40)}...
288
- ${figmaLinks ? `Figma: ${figmaLinks}` : ''}
289
- ${ann.backendEndpoints?.length ? `Endpoints: ${ann.backendEndpoints.join(', ')}` : ''}
290
- ${ann.corrections && Array.isArray(ann.corrections) && ann.corrections.length > 0 ? `Feedback: ${ann.corrections[ann.corrections.length - 1].text}` : ''}
291
- Task: ${ann.message}
292
- Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}`
493
+ ...ann,
494
+ _ia_fix_instruction: `### INSTRUCCIONES PARA LA IA (IMPORTANTE) ###
495
+ A continuación se presenta un ticket que requiere tu atención.
496
+ DEBES RESPONDER EN ESPAÑOL.
497
+ Proyecto: ${rawData.name || 'ViewGate'}
498
+ Endpoints: ${ann.backendEndpoints?.length ? ann.backendEndpoints.join(', ') : 'Ninguno'}
499
+ Feedback previo: ${ann.corrections && Array.isArray(ann.corrections) && ann.corrections.length > 0 ? ann.corrections[ann.corrections.length - 1].text : 'Ninguno'}
500
+ Tarea: ${ann.message}
501
+ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
502
+ (RECUERDA: Toda tu respuesta, comentarios y análisis DEBEN estar en ESPAÑOL)`
293
503
  };
294
504
  });
295
505
  const langHint = rawData.preferredLanguage === 'es'
296
- ? "[INSTRUCTION: Project is in SPANISH. Provide all comments and aiAnalysis in SPANISH.]\n\n"
297
- : "[INSTRUCTION: Provide all comments and aiAnalysis in English.]\n\n";
506
+ ? "\n*** [INSTRUCTION: Project is in SPANISH. Provide all comments, appliedChanges, and aiAnalysis in SPANISH ONLY.] ***\n\n"
507
+ : "\n*** [INSTRUCTION: Provide all comments and analysis in English.] ***\n\n";
298
508
  return {
299
509
  content: [{ type: "text", text: langHint + JSON.stringify({ preferredLanguage: rawData.preferredLanguage || 'en', annotations: annotationsWithTips }, null, 2) }],
300
510
  };
@@ -354,8 +564,8 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}`
354
564
  throw new Error(`Backend responded with ${response.status}`);
355
565
  const data = (await response.json());
356
566
  const langHint = data.preferredLanguage === 'es'
357
- ? "[INSTRUCTION: Project is in SPANISH. Provide all comments and aiAnalysis in SPANISH.]\n\n"
358
- : (data.preferredLanguage === 'en' ? "[INSTRUCTION: Provide all comments and aiAnalysis in English.]\n\n" : "");
567
+ ? "\n*** [INSTRUCTION: Project is in SPANISH. Provide all comments, appliedChanges, and aiAnalysis in SPANISH ONLY.] ***\n\n\n\n"
568
+ : (data.preferredLanguage === 'en' ? "\n*** [INSTRUCTION: Provide all comments and analysis in English.] ***\n\n\n\n" : "");
359
569
  return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
360
570
  }
361
571
  case "sync_endpoints": {
@@ -387,8 +597,8 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}`
387
597
  throw new Error(`Backend responded with ${response.status}`);
388
598
  const data = (await response.json());
389
599
  const langHint = data.preferredLanguage === 'es'
390
- ? "[INSTRUCTION: Project is in SPANISH. Provide all comments and aiAnalysis in SPANISH.]\n\n"
391
- : (data.preferredLanguage === 'en' ? "[INSTRUCTION: Provide all comments and aiAnalysis in English.]\n\n" : "");
600
+ ? "\n*** [INSTRUCTION: Project is in SPANISH. Provide all comments, appliedChanges, and aiAnalysis in SPANISH ONLY.] ***\n\n\n\n"
601
+ : (data.preferredLanguage === 'en' ? "\n*** [INSTRUCTION: Provide all comments and analysis in English.] ***\n\n\n\n" : "");
392
602
  return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
393
603
  }
394
604
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viewgate-mcp",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "viewgate-mcp": "./dist/index.js"