viewgate-mcp 1.0.42 → 1.0.44

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 +96 -289
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -23,8 +23,6 @@ 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";
28
26
  const __filename = fileURLToPath(import.meta.url);
29
27
  const __dirname = path.dirname(__filename);
30
28
  dotenv.config({ path: path.join(__dirname, "..", ".env") });
@@ -64,7 +62,7 @@ function createMcpServer(apiKey, personalKey) {
64
62
  },
65
63
  {
66
64
  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.",
65
+ 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.",
68
66
  inputSchema: {
69
67
  type: "object",
70
68
  properties: {
@@ -73,6 +71,19 @@ function createMcpServer(apiKey, personalKey) {
73
71
  },
74
72
  },
75
73
  },
74
+ {
75
+ name: "mark_ui_component_generated",
76
+ description: "Mark a UI component as generated by submitting its code and props to the backend. This enables the dashboard iframe preview (/preview/:id).",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ id: { type: "string", description: "Internal UI component id." },
81
+ code: { type: "string", description: "Generated component code used for iframe preview." },
82
+ props: { type: "object", description: "Props object used for iframe preview." }
83
+ },
84
+ required: ["id", "code"]
85
+ }
86
+ },
76
87
  {
77
88
  name: "get_annotations",
78
89
  description: "Fetch feedback. Keys ('VG-XXXX') or IDs. Workflow: 1. Fetch, 2. Fix, 3. Mark Ready.",
@@ -88,6 +99,19 @@ function createMcpServer(apiKey, personalKey) {
88
99
  },
89
100
  },
90
101
  },
102
+ {
103
+ name: "get_ui_improvements",
104
+ description: "Fetch pending UI/UX improvement tickets. STRICTLY LIMITED TO VISUAL/CSS CHANGES. Do not add functional logic or modify backend integrations.",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ limit: { type: "number", description: "Max results.", default: 5 },
109
+ search: { type: "string", description: "Search message/file." },
110
+ key: { type: "string", description: "VG-XXXX key." },
111
+ ids: { type: "string", description: "Internal IDs." }
112
+ },
113
+ },
114
+ },
91
115
  {
92
116
  name: "mark_annotation_ready",
93
117
  description: "Mark as ready/applied. Use internal IDs. IMPORTANT: appliedChanges must be in the project's preferredLanguage (e.g. SPANISH).",
@@ -189,247 +213,6 @@ function createMcpServer(apiKey, personalKey) {
189
213
  ],
190
214
  };
191
215
  });
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 readJsonIfExists = async (p) => {
253
- try {
254
- const txt = await fsp.readFile(p, 'utf8');
255
- return JSON.parse(txt);
256
- }
257
- catch {
258
- return null;
259
- }
260
- };
261
- const looksLikeFrontendPackageJson = (pkg) => {
262
- const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
263
- const hasReact = typeof deps.react === 'string';
264
- const hasViteOrNext = typeof deps.vite === 'string' || typeof deps.next === 'string';
265
- return hasReact && hasViteOrNext;
266
- };
267
- const listSubdirs = async (dir) => {
268
- try {
269
- const entries = await fsp.readdir(dir, { withFileTypes: true });
270
- return entries.filter((e) => e.isDirectory()).map((e) => path.join(dir, e.name));
271
- }
272
- catch {
273
- return [];
274
- }
275
- };
276
- const findFrontendRoot = async () => {
277
- // In many MCP clients, INIT_CWD points to the directory where `npx viewgate-mcp` was launched.
278
- const baseDir = process.env.VIEWGATE_WORKSPACE || process.env.INIT_CWD || process.cwd();
279
- // Scan baseDir and one level deep for candidate frontends.
280
- const candidates = [baseDir, ...(await listSubdirs(baseDir))];
281
- const hits = [];
282
- for (const c of candidates) {
283
- const pkg = await readJsonIfExists(path.join(c, 'package.json'));
284
- if (pkg && looksLikeFrontendPackageJson(pkg))
285
- hits.push(c);
286
- }
287
- if (hits.length === 0)
288
- return null;
289
- if (hits.length === 1)
290
- return hits[0];
291
- // Prefer common names when multiple projects exist.
292
- const preferred = hits.find((h) => {
293
- const name = path.basename(h).toLowerCase();
294
- return name === 'dashboard' || name === 'view-gate-dashboard' || name.includes('dashboard');
295
- });
296
- return preferred || hits[0];
297
- };
298
- const resolveComponentsDir = async () => {
299
- const frontendRoot = await findFrontendRoot();
300
- const cwd = process.cwd();
301
- // Order matters: prefer src/components for SPA projects like Vite.
302
- const rootsToTry = [
303
- ...(frontendRoot ? [frontendRoot] : []),
304
- cwd,
305
- ];
306
- for (const root of rootsToTry) {
307
- const c1 = path.join(root, 'src', 'components');
308
- if (fs.existsSync(c1))
309
- return c1;
310
- const c2 = path.join(root, 'components');
311
- if (fs.existsSync(c2))
312
- return c2;
313
- }
314
- // Fallback: create in src/components under detected frontendRoot if present.
315
- if (frontendRoot)
316
- return path.join(frontendRoot, 'src', 'components');
317
- return path.join(cwd, 'components');
318
- };
319
- const writeComponentFile = async (componentType, model) => {
320
- const componentsDir = await resolveComponentsDir();
321
- await fsp.mkdir(componentsDir, { recursive: true });
322
- const baseName = toPascalCase(componentType) || 'Component';
323
- const projectRoot = path.dirname(componentsDir);
324
- const hasTs = fs.existsSync(path.join(projectRoot, 'tsconfig.json'));
325
- const ext = hasTs ? 'tsx' : 'jsx';
326
- const fileBase = `${baseName}.${ext}`;
327
- let filePath = path.join(componentsDir, fileBase);
328
- if (fs.existsSync(filePath)) {
329
- // never overwrite existing
330
- let i = 2;
331
- while (fs.existsSync(path.join(componentsDir, `${baseName}.${i}.${ext}`)))
332
- i++;
333
- filePath = path.join(componentsDir, `${baseName}.${i}.${ext}`);
334
- }
335
- const isTs = ext === 'tsx';
336
- const commonOptional = model.common.map((p) => `${p}${isTs ? '?: any' : ''}`);
337
- const required = model.required.map((p) => `${p}${isTs ? ': any' : ''}`);
338
- const ariaIndex = model.hasAriaIndex ? (isTs ? `\n [key: \`aria-\${string}\`]: any;` : '') : '';
339
- const dataIndex = model.hasDataIndex ? (isTs ? `\n [key: \`data-\${string}\`]: any;` : '') : '';
340
- const propsBlock = isTs
341
- ? `export type ${baseName}Props = {\n${[
342
- model.hasAs ? ` as?: React.ElementType;` : null,
343
- model.hasChildren ? ` children?: React.ReactNode;` : null,
344
- ...required.map((l) => ` ${l};`),
345
- ...commonOptional.map((l) => ` ${l};`),
346
- ].filter(Boolean).join('\n')}${ariaIndex}${dataIndex}\n};`
347
- : '';
348
- const destructure = [
349
- model.hasAs ? 'as: Comp = "div"' : null,
350
- model.hasChildren ? 'children' : null,
351
- ...model.required,
352
- ...model.common,
353
- '...rest'
354
- ].filter(Boolean).join(', ');
355
- const jsxTag = model.hasAs ? '<Comp' : '<div';
356
- const jsxClose = model.hasAs ? '</Comp>' : '</div>';
357
- const content = isTs
358
- ? `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`
359
- : `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`;
360
- await fsp.writeFile(filePath, content, 'utf8');
361
- return { filePath, fileName: path.basename(filePath) };
362
- };
363
- const makePreviewSvg = (title, props) => {
364
- const safeTitle = (title || '').replace(/[<>]/g, '');
365
- const list = (props || []).slice(0, 14).map((p) => p.replace(/[<>]/g, '')).join(' • ');
366
- const text = `${safeTitle}${list ? ' — ' + list : ''}`;
367
- 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>`;
368
- };
369
- const buildDefaultProps = (propNames) => {
370
- const defaults = {};
371
- for (const p of propNames || []) {
372
- if (!p)
373
- continue;
374
- const name = String(p);
375
- if (name === 'disabled' || name === 'loading' || name === 'fullWidth') {
376
- defaults[name] = false;
377
- continue;
378
- }
379
- if (name === 'variant') {
380
- defaults[name] = 'primary';
381
- continue;
382
- }
383
- if (name === 'size') {
384
- defaults[name] = 'md';
385
- continue;
386
- }
387
- if (name === 'type') {
388
- defaults[name] = 'button';
389
- continue;
390
- }
391
- if (name === 'href') {
392
- defaults[name] = '#';
393
- continue;
394
- }
395
- if (name.toLowerCase().startsWith('on')) {
396
- defaults[name] = null;
397
- continue;
398
- }
399
- if (name === 'iconLeft' || name === 'iconRight') {
400
- defaults[name] = null;
401
- continue;
402
- }
403
- // Fallback
404
- defaults[name] = '';
405
- }
406
- return defaults;
407
- };
408
- const buildComponentCode = (componentType, requiredProps) => {
409
- const safeName = toPascalCase(componentType) || 'Component';
410
- const props = (requiredProps || []).filter(Boolean);
411
- const destructure = props.length > 0 ? `{ ${props.join(', ')} }` : 'props';
412
- // IMPORTANT: This code is executed inside a browser ESM module where React is already imported.
413
- // Avoid JSX to keep it runnable without a build step.
414
- const lines = [];
415
- lines.push(`const ${safeName} = (${destructure}) => {`);
416
- lines.push(` return React.createElement(`);
417
- lines.push(` "div",`);
418
- lines.push(` { style: { fontFamily: 'ui-sans-serif, system-ui', padding: 12 } },`);
419
- lines.push(` React.createElement(`);
420
- lines.push(` "div",`);
421
- lines.push(` { style: { fontSize: 14, fontWeight: 700, marginBottom: 8 } },`);
422
- lines.push(` ${JSON.stringify(safeName)}
423
- ),`);
424
- lines.push(` React.createElement(`);
425
- lines.push(` "pre",`);
426
- lines.push(` { style: { fontSize: 12, opacity: 0.8, whiteSpace: 'pre-wrap' } },`);
427
- lines.push(` JSON.stringify({ ${props.join(', ')} }, null, 2)
428
- )`);
429
- lines.push(` );`);
430
- lines.push(`};`);
431
- return { exportName: safeName, code: lines.join('\n') };
432
- };
433
216
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
434
217
  const toolName = request.params.name;
435
218
  console.error(`[MCP] Handling tool call: ${toolName}`);
@@ -458,7 +241,6 @@ function createMcpServer(apiKey, personalKey) {
458
241
  case "generate_ui_components": {
459
242
  const args = request.params.arguments;
460
243
  const limit = Math.min(args.limit || 1, 10);
461
- const targetComponentsDir = await resolveComponentsDir();
462
244
  const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
463
245
  fetchUrl.searchParams.append("limit", limit.toString());
464
246
  fetchUrl.searchParams.append("status", "pending");
@@ -482,51 +264,29 @@ function createMcpServer(apiKey, personalKey) {
482
264
  const componentType = item.componentType;
483
265
  const requiredProps = (item.requiredProps || []);
484
266
  const commonProps = (item.commonProps || []);
485
- const defaults = buildDefaultProps(requiredProps);
486
- const built = buildComponentCode(componentType, requiredProps);
487
- // Fast preview generation (SVG placeholder) + upload
488
- const svg = makePreviewSvg(componentType, requiredProps);
489
- const svg64 = Buffer.from(svg, 'utf8').toString('base64');
490
- const imageDataUrl = `data:image/svg+xml;base64,${svg64}`;
491
- const uploadResp = await fetch(`${BACKEND_URL}/api/screenshots/upload`, {
492
- method: 'POST',
493
- headers: {
494
- 'Content-Type': 'application/json',
495
- 'x-api-key': apiKey,
496
- ...(personalKey ? { 'x-personal-key': personalKey } : {})
497
- },
498
- body: JSON.stringify({ image: imageDataUrl })
499
- });
500
- if (!uploadResp.ok) {
501
- const errorBody = await uploadResp.text();
502
- throw new Error(`Preview upload failed (${uploadResp.status}): ${errorBody}`);
503
- }
504
- const uploaded = (await uploadResp.json());
505
- const previewUrl = uploaded?.url || null;
506
- const markResp = await fetch(`${BACKEND_URL}/api/mcp/components/${item._id}/generated`, {
507
- method: 'PATCH',
508
- headers: {
509
- 'Content-Type': 'application/json',
510
- 'x-api-key': apiKey,
511
- ...(personalKey ? { 'x-personal-key': personalKey } : {})
512
- },
513
- body: JSON.stringify({ previewImage: previewUrl, code: built.code, props: defaults })
514
- });
515
- if (!markResp.ok) {
516
- const errorBody = await markResp.text();
517
- throw new Error(`Mark generated failed (${markResp.status}): ${errorBody}`);
518
- }
519
- const marked = (await markResp.json());
267
+ const llmInstruction = {
268
+ componentType,
269
+ requiredProps,
270
+ commonProps,
271
+ figmaUrl: item.figmaUrl,
272
+ htmlContent: item.htmlContent,
273
+ cssContent: item.cssContent,
274
+ sourceType: item.sourceType,
275
+ constraints: {
276
+ mustBeFunctional: true,
277
+ mustSupportRequiredProps: true,
278
+ mustSupportCommonProps: true,
279
+ avoidBreakingChanges: true,
280
+ useProvidedHtmlAndCssIfAvailable: true
281
+ }
282
+ };
520
283
  results.push({
521
284
  componentType,
522
285
  figmaUrl: item.figmaUrl,
523
286
  requiredProps,
524
- target: {
525
- componentsDir: targetComponentsDir,
526
- suggestedFileName: `${toPascalCase(componentType) || 'Component'}.jsx`
527
- },
528
- previewImage: previewUrl,
529
- db: marked?.data?._id || item._id
287
+ commonProps,
288
+ db: item._id,
289
+ llmInstruction
530
290
  });
531
291
  }
532
292
  return {
@@ -534,14 +294,34 @@ function createMcpServer(apiKey, personalKey) {
534
294
  type: "text",
535
295
  text: JSON.stringify({
536
296
  ok: true,
537
- generated: results.length,
538
- instruction: `IMPORTANTE: Generar/guardar el componente React real dentro del proyecto frontend detectado. Ruta objetivo sugerida: ${targetComponentsDir}. No escribir en rutas globales del editor.`,
539
- targetComponentsDir,
297
+ generated: 0,
298
+ instruction: "IMPORTANTE: Este tool NO debe generar código ni escribir archivos. Solo entrega al LLM la especificación (componentType + props) para que el LLM implemente un componente real y funcional en el repo.",
540
299
  results
541
300
  }, null, 2)
542
301
  }]
543
302
  };
544
303
  }
304
+ case "mark_ui_component_generated": {
305
+ const args = request.params.arguments;
306
+ if (!args?.id || !args?.code) {
307
+ throw new Error("id and code are required");
308
+ }
309
+ const response = await fetch(`${BACKEND_URL}/api/mcp/components/${args.id}/generated`, {
310
+ method: 'PATCH',
311
+ headers: {
312
+ 'Content-Type': 'application/json',
313
+ 'x-api-key': apiKey,
314
+ ...(personalKey ? { 'x-personal-key': personalKey } : {})
315
+ },
316
+ body: JSON.stringify({ code: args.code, props: args.props })
317
+ });
318
+ if (!response.ok) {
319
+ const errorBody = await response.text();
320
+ throw new Error(`Backend responded with ${response.status}: ${errorBody}`);
321
+ }
322
+ const data = await response.json();
323
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
324
+ }
545
325
  case "get_annotations": {
546
326
  const args = request.params.arguments;
547
327
  const limit = args.limit || 3;
@@ -714,6 +494,33 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
714
494
  : (data.preferredLanguage === 'en' ? "\n*** [INSTRUCTION: Provide all comments and analysis in English.] ***\n\n\n\n" : "");
715
495
  return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
716
496
  }
497
+ case "get_ui_improvements": {
498
+ const args = request.params.arguments;
499
+ const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/ui-improvements`);
500
+ if (args.limit)
501
+ fetchUrl.searchParams.append("limit", args.limit.toString());
502
+ if (args.search)
503
+ fetchUrl.searchParams.append("search", args.search);
504
+ if (args.key)
505
+ fetchUrl.searchParams.append("key", args.key);
506
+ if (args.ids)
507
+ fetchUrl.searchParams.append("ids", args.ids);
508
+ const response = await fetch(fetchUrl, {
509
+ headers: {
510
+ 'x-api-key': apiKey,
511
+ ...(personalKey ? { 'x-personal-key': personalKey } : {})
512
+ }
513
+ });
514
+ if (!response.ok) {
515
+ const errorBody = await response.text();
516
+ throw new Error(`Backend responded with ${response.status}: ${errorBody}`);
517
+ }
518
+ const data = (await response.json());
519
+ const langHint = data.preferredLanguage === 'es'
520
+ ? "\n*** [INSTRUCTION: Project is in SPANISH. Provide all CSS/Visual changes in SPANISH comments if requested.] ***\n\n\n\n"
521
+ : "";
522
+ return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
523
+ }
717
524
  case "sync_endpoints": {
718
525
  const args = request.params.arguments;
719
526
  const response = await fetch(`${BACKEND_URL}/api/mcp/sync-endpoints`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viewgate-mcp",
3
- "version": "1.0.42",
3
+ "version": "1.0.44",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "viewgate-mcp": "./dist/index.js"