Skip to content

Commit b24077c

Browse files
committed
feat(code-generation): add detectTemplateMismatch, reorder next steps on pattern mismatch
1 parent c765680 commit b24077c

1 file changed

Lines changed: 92 additions & 13 deletions

File tree

packages/pluggable-widgets-mcp/src/tools/code-generation.tools.ts

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,18 @@ const generateWidgetCodeSchema = z.object({
104104
widgetPath: z.string().min(1).describe("Absolute path to the scaffolded widget directory"),
105105
description: z.string().min(1).describe("Description of what the widget should do"),
106106
properties: z
107-
.array(propertyDefinitionSchema)
108-
.optional()
107+
.preprocess(v => {
108+
// MCP clients (e.g. Maia) sometimes send JSON arrays as a stringified string.
109+
// Parse it transparently so validation still runs on the actual array contents.
110+
if (typeof v === "string") {
111+
try {
112+
return JSON.parse(v);
113+
} catch {
114+
return v; // let Zod report the type error
115+
}
116+
}
117+
return v;
118+
}, z.array(propertyDefinitionSchema).optional())
109119
.describe("Array of property definitions. If not provided, returns suggestions."),
110120
widgetPattern: z
111121
.enum(["display", "button", "input", "container", "dataList"])
@@ -225,12 +235,15 @@ async function cleanupScaffoldFiles(widgetPath: string, widgetName: string): Pro
225235
const packageXmlPath = join(srcDir, "package.xml");
226236
const widgetNameLower = widgetName.toLowerCase();
227237
let version = "1.0.0";
238+
let packagePath = "mendix";
228239
try {
229240
const pkgJson = JSON.parse(await readFile(join(widgetPath, "package.json"), "utf-8"));
230241
if (pkgJson.version) version = pkgJson.version;
242+
if (pkgJson.packagePath) packagePath = pkgJson.packagePath;
231243
} catch {
232244
/* use default */
233245
}
246+
const filePath = `${packagePath.replace(/\./g, "/")}/${widgetNameLower}`;
234247
const packageXml = [
235248
'<?xml version="1.0" encoding="utf-8" ?>',
236249
'<package xmlns="http://www.mendix.com/package/1.0/">',
@@ -239,7 +252,7 @@ async function cleanupScaffoldFiles(widgetPath: string, widgetName: string): Pro
239252
` <widgetFile path="${widgetName}.xml"/>`,
240253
" </widgetFiles>",
241254
" <files>",
242-
` <file path="mendix/${widgetNameLower}"/>`,
255+
` <file path="${filePath}"/>`,
243256
" </files>",
244257
" </clientModule>",
245258
"</package>"
@@ -418,6 +431,55 @@ function getPatternDescription(pattern: WidgetPattern): string {
418431
}
419432
}
420433

434+
/**
435+
* Detects a mismatch between the selected widget pattern and the provided properties.
436+
*
437+
* Returns a warning string when the pattern's key property types are absent,
438+
* or null when the properties satisfy the pattern's requirements.
439+
*/
440+
export function detectTemplateMismatch(pattern: WidgetPattern, properties: PropertyDefinition[]): string | null {
441+
const types = properties.map(p => p.type);
442+
443+
switch (pattern) {
444+
case "button": {
445+
const warnings: string[] = [];
446+
if (!types.includes("action")) {
447+
warnings.push("button will be permanently disabled (no action property)");
448+
}
449+
if (!types.includes("textTemplate") && !types.includes("string")) {
450+
warnings.push("button will render empty text (no textTemplate or string property for caption)");
451+
}
452+
return warnings.length > 0 ? warnings.join("; ") : null;
453+
}
454+
case "input":
455+
if (!types.includes("attribute")) {
456+
return "no attribute property for data binding — input pattern needs an attribute to read/write values";
457+
}
458+
return null;
459+
case "display":
460+
if (!types.includes("textTemplate") && !types.includes("expression") && !types.includes("string")) {
461+
return "read-only display component with no dynamic text source — only primitive types found; customize the generated code or add a textTemplate/expression property";
462+
}
463+
return null;
464+
case "container":
465+
if (!types.includes("widgets")) {
466+
return "no child content slot — container pattern needs a widgets property for nested widget content";
467+
}
468+
return null;
469+
case "dataList": {
470+
const missing: string[] = [];
471+
if (!types.includes("datasource")) missing.push("datasource");
472+
if (!types.includes("widgets")) missing.push("widgets");
473+
if (missing.length > 0) {
474+
return `dataList pattern is missing required properties: ${missing.join(", ")}`;
475+
}
476+
return null;
477+
}
478+
default:
479+
return null;
480+
}
481+
}
482+
421483
// =============================================================================
422484
// Tool Handler
423485
// =============================================================================
@@ -491,6 +553,7 @@ async function handleGenerateWidgetCode(args: GenerateWidgetCodeInput): Promise<
491553
const filesToWrite = [
492554
{ path: `src/${widgetName}.xml`, content: xmlResult.xml },
493555
{ path: `src/${widgetName}.tsx`, content: tsxResult.mainComponent },
556+
{ path: `src/${widgetName}.editorPreview.tsx`, content: tsxResult.editorPreview! },
494557
{ path: `src/ui/${widgetName}.scss`, content: `.widget-${widgetName.toLowerCase()} {\n}\n` },
495558
{ path: `src/.widget-definition.json`, content: JSON.stringify(widgetDefinition, null, 2) }
496559
];
@@ -518,24 +581,40 @@ async function handleGenerateWidgetCode(args: GenerateWidgetCodeInput): Promise<
518581

519582
// Build success response
520583
const propSummary = properties.map(p => p.key).join(", ");
584+
const mismatch = detectTemplateMismatch(pattern, properties as PropertyDefinition[]);
585+
586+
const lines = [
587+
`✅ Widget code generated successfully!`,
588+
"",
589+
`📁 Files written:`,
590+
` • src/${widgetName}.xml - Widget definition with ${properties.length} properties (${propSummary})`,
591+
` • src/${widgetName}.tsx - Component using ${pattern} pattern`,
592+
` • src/ui/${widgetName}.scss - Empty SCSS placeholder`,
593+
` • src/.widget-definition.json - Widget definition snapshot (used by update-widget-properties)`
594+
];
521595

522-
return createToolResponse(
523-
[
524-
`✅ Widget code generated successfully!`,
596+
if (mismatch) {
597+
lines.push("", `⚠️ Template notice: ${mismatch}`);
598+
lines.push(
525599
"",
526-
`📁 Files written:`,
527-
` • src/${widgetName}.xml - Widget definition with ${properties.length} properties (${propSummary})`,
528-
` • src/${widgetName}.tsx - Component using ${pattern} pattern`,
529-
` • src/ui/${widgetName}.scss - Empty SCSS placeholder`,
530-
` • src/.widget-definition.json - Widget definition snapshot (used by update-widget-properties)`,
600+
`🔨 Next steps:`,
601+
` 1. Review and customize the generated code (use write-widget-file to update src/${widgetName}.tsx)`,
602+
` 2. Run build-widget to compile and validate`,
603+
` 3. Update src/${widgetName}.editorPreview.tsx for Studio Pro design mode preview`,
604+
` 4. Test in Mendix Studio Pro`
605+
);
606+
} else {
607+
lines.push(
531608
"",
532609
`🔨 Next steps:`,
533610
` 1. Run build-widget to compile and validate`,
534611
` 2. Review and customize generated code`,
535612
` 3. Update src/${widgetName}.editorPreview.tsx for Studio Pro design mode preview`,
536613
` 4. Test in Mendix Studio Pro`
537-
].join("\n")
538-
);
614+
);
615+
}
616+
617+
return createToolResponse(lines.join("\n"));
539618
} catch (error) {
540619
const message = error instanceof Error ? error.message : String(error);
541620
console.error(`[code-generation] Error: ${message}`);

0 commit comments

Comments
 (0)