viewgate-mcp 1.0.42 → 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.
- package/dist/index.js +51 -288
- 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,247 +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
|
-
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
203
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
434
204
|
const toolName = request.params.name;
|
|
435
205
|
console.error(`[MCP] Handling tool call: ${toolName}`);
|
|
@@ -458,7 +228,6 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
458
228
|
case "generate_ui_components": {
|
|
459
229
|
const args = request.params.arguments;
|
|
460
230
|
const limit = Math.min(args.limit || 1, 10);
|
|
461
|
-
const targetComponentsDir = await resolveComponentsDir();
|
|
462
231
|
const fetchUrl = new URL(`${BACKEND_URL}/api/mcp/components`);
|
|
463
232
|
fetchUrl.searchParams.append("limit", limit.toString());
|
|
464
233
|
fetchUrl.searchParams.append("status", "pending");
|
|
@@ -482,51 +251,25 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
482
251
|
const componentType = item.componentType;
|
|
483
252
|
const requiredProps = (item.requiredProps || []);
|
|
484
253
|
const commonProps = (item.commonProps || []);
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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());
|
|
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
|
+
};
|
|
520
266
|
results.push({
|
|
521
267
|
componentType,
|
|
522
268
|
figmaUrl: item.figmaUrl,
|
|
523
269
|
requiredProps,
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
},
|
|
528
|
-
previewImage: previewUrl,
|
|
529
|
-
db: marked?.data?._id || item._id
|
|
270
|
+
commonProps,
|
|
271
|
+
db: item._id,
|
|
272
|
+
llmInstruction
|
|
530
273
|
});
|
|
531
274
|
}
|
|
532
275
|
return {
|
|
@@ -534,14 +277,34 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
534
277
|
type: "text",
|
|
535
278
|
text: JSON.stringify({
|
|
536
279
|
ok: true,
|
|
537
|
-
generated:
|
|
538
|
-
instruction:
|
|
539
|
-
targetComponentsDir,
|
|
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.",
|
|
540
282
|
results
|
|
541
283
|
}, null, 2)
|
|
542
284
|
}]
|
|
543
285
|
};
|
|
544
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) }] };
|
|
307
|
+
}
|
|
545
308
|
case "get_annotations": {
|
|
546
309
|
const args = request.params.arguments;
|
|
547
310
|
const limit = args.limit || 3;
|