viewgate-mcp 1.0.41 → 1.0.43

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 +60 -218
  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") });
@@ -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.",
@@ -189,183 +200,6 @@ function createMcpServer(apiKey, personalKey) {
189
200
  ],
190
201
  };
191
202
  });
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
203
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
370
204
  const toolName = request.params.name;
371
205
  console.error(`[MCP] Handling tool call: ${toolName}`);
@@ -417,51 +251,59 @@ function createMcpServer(apiKey, personalKey) {
417
251
  const componentType = item.componentType;
418
252
  const requiredProps = (item.requiredProps || []);
419
253
  const commonProps = (item.commonProps || []);
420
- const model = buildPropModel(commonProps, requiredProps);
421
- const fileInfo = await writeComponentFile(componentType, model);
422
- // Fast preview generation (SVG placeholder) + upload
423
- const svg = makePreviewSvg(componentType, requiredProps);
424
- const svg64 = Buffer.from(svg, 'utf8').toString('base64');
425
- const imageDataUrl = `data:image/svg+xml;base64,${svg64}`;
426
- const uploadResp = await fetch(`${BACKEND_URL}/api/screenshots/upload`, {
427
- method: 'POST',
428
- headers: {
429
- 'Content-Type': 'application/json',
430
- 'x-api-key': apiKey,
431
- ...(personalKey ? { 'x-personal-key': personalKey } : {})
432
- },
433
- body: JSON.stringify({ image: imageDataUrl })
434
- });
435
- if (!uploadResp.ok) {
436
- const errorBody = await uploadResp.text();
437
- throw new Error(`Preview upload failed (${uploadResp.status}): ${errorBody}`);
438
- }
439
- const uploaded = (await uploadResp.json());
440
- const previewUrl = uploaded?.url || null;
441
- const markResp = await fetch(`${BACKEND_URL}/api/mcp/components/${item._id}/generated`, {
442
- method: 'PATCH',
443
- headers: {
444
- 'Content-Type': 'application/json',
445
- 'x-api-key': apiKey,
446
- ...(personalKey ? { 'x-personal-key': personalKey } : {})
447
- },
448
- body: JSON.stringify({ previewImage: previewUrl })
449
- });
450
- if (!markResp.ok) {
451
- const errorBody = await markResp.text();
452
- throw new Error(`Mark generated failed (${markResp.status}): ${errorBody}`);
453
- }
454
- const marked = (await markResp.json());
254
+ const llmInstruction = {
255
+ componentType,
256
+ requiredProps,
257
+ commonProps,
258
+ figmaUrl: item.figmaUrl,
259
+ constraints: {
260
+ mustBeFunctional: true,
261
+ mustSupportRequiredProps: true,
262
+ mustSupportCommonProps: true,
263
+ avoidBreakingChanges: true,
264
+ }
265
+ };
455
266
  results.push({
456
267
  componentType,
457
268
  figmaUrl: item.figmaUrl,
458
269
  requiredProps,
459
- written: fileInfo,
460
- previewImage: previewUrl,
461
- db: marked?.data?._id || item._id
270
+ commonProps,
271
+ db: item._id,
272
+ llmInstruction
462
273
  });
463
274
  }
464
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, generated: results.length, results }, null, 2) }] };
275
+ return {
276
+ content: [{
277
+ type: "text",
278
+ text: JSON.stringify({
279
+ ok: true,
280
+ generated: 0,
281
+ 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.",
282
+ results
283
+ }, null, 2)
284
+ }]
285
+ };
286
+ }
287
+ case "mark_ui_component_generated": {
288
+ const args = request.params.arguments;
289
+ if (!args?.id || !args?.code) {
290
+ throw new Error("id and code are required");
291
+ }
292
+ const response = await fetch(`${BACKEND_URL}/api/mcp/components/${args.id}/generated`, {
293
+ method: 'PATCH',
294
+ headers: {
295
+ 'Content-Type': 'application/json',
296
+ 'x-api-key': apiKey,
297
+ ...(personalKey ? { 'x-personal-key': personalKey } : {})
298
+ },
299
+ body: JSON.stringify({ code: args.code, props: args.props })
300
+ });
301
+ if (!response.ok) {
302
+ const errorBody = await response.text();
303
+ throw new Error(`Backend responded with ${response.status}: ${errorBody}`);
304
+ }
305
+ const data = await response.json();
306
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
465
307
  }
466
308
  case "get_annotations": {
467
309
  const args = request.params.arguments;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viewgate-mcp",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "viewgate-mcp": "./dist/index.js"