66import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
77import { spawn } from "node:child_process" ;
88import { existsSync } from "node:fs" ;
9- import { join } from "node:path" ;
9+ import { readFile } from "node:fs/promises" ;
10+ import { join , normalize , sep } from "node:path" ;
1011import { z } from "zod" ;
1112import { GENERATIONS_DIR } from "@/config" ;
1213import type { ToolContext , ToolResponse } from "./types" ;
1314import { ProgressTracker } from "./utils/progress-tracker" ;
14- import {
15- createStructuredError ,
16- createStructuredErrorResponse ,
17- createToolResponse ,
18- type StructuredError
19- } from "./utils/response" ;
15+ import { createStructuredError , createStructuredErrorResponse , createToolResponse } from "./utils/response" ;
2016import { findMpkFile } from "./utils/mpk" ;
2117import { isPathAllowed } from "./utils/sandbox" ;
2218import type { SessionState } from "./session-state" ;
@@ -33,7 +29,7 @@ type BuildWidgetInput = z.infer<typeof buildWidgetSchema>;
3329/**
3430 * Parsed error with location information.
3531 */
36- interface ParsedError {
32+ export interface ParsedError {
3733 message : string ;
3834 file ?: string ;
3935 line ?: number ;
@@ -137,8 +133,8 @@ function parseBuildOutput(stdout: string, stderr: string): BuildResult {
137133
138134 const lines = output . split ( "\n" ) ;
139135
140- for ( let i = 0 ; i < lines . length ; i ++ ) {
141- const trimmed = lines [ i ] . trim ( ) ;
136+ for ( const line of lines ) {
137+ const trimmed = line . trim ( ) ;
142138 if ( ! trimmed ) continue ;
143139
144140 // TypeScript errors (try to parse with location)
@@ -221,6 +217,80 @@ function parseBuildOutput(stdout: string, stderr: string): BuildResult {
221217 } ;
222218}
223219
220+ /**
221+ * Formats a build failure response for Maia, including:
222+ * - All errors with file/line/column/code
223+ * - Content of every source file that appears in the error list
224+ *
225+ * Embedding file content lets Maia fix errors without an extra read-widget-file
226+ * round-trip. Output format is designed to be read by an AI agent.
227+ */
228+ export async function formatBuildFailureResponse ( errors : ParsedError [ ] , widgetPath : string ) : Promise < string > {
229+ // Format error list — each error gets code, location (file line N col N), and message
230+ const errorLines = errors . map ( e => {
231+ const loc = e . file
232+ ? `${ e . file } ${ e . line != null ? ` line ${ e . line } ` : "" } ${ e . column != null ? ` col ${ e . column } ` : "" } `
233+ : null ;
234+ const code = e . tsCode ? `[${ e . tsCode } ]` : `[${ e . category } ]` ;
235+ const locStr = loc ? ` ${ loc } —` : "" ;
236+ return ` ${ code } ${ locStr } ${ e . message } ` ;
237+ } ) ;
238+
239+ // Collect unique source files that appear in errors
240+ const uniqueFiles = [ ...new Set ( errors . map ( e => e . file ) . filter ( ( f ) : f is string => ! ! f ) ) ] ;
241+
242+ // Read each failing file (skip if not found — don't throw)
243+ const fileSections : string [ ] = [ ] ;
244+ for ( const relPath of uniqueFiles ) {
245+ // Block path traversal: only allow files under widgetPath
246+ const normalizedBase = normalize ( widgetPath ) ;
247+ const fullPath = normalize ( join ( widgetPath , relPath ) ) ;
248+ if ( ! fullPath . startsWith ( normalizedBase + sep ) && fullPath !== normalizedBase ) continue ;
249+ if ( ! existsSync ( fullPath ) ) continue ;
250+
251+ try {
252+ const content = await readFile ( fullPath , "utf-8" ) ;
253+ fileSections . push ( `--- ${ relPath } ---\n${ content } ` ) ;
254+ } catch {
255+ // Skip unreadable files silently
256+ }
257+ }
258+
259+ const lines = [
260+ `❌ Build failed — ${ errors . length } error(s). Fix the errors below, write with write-widget-file, then retry build-widget (max 3 attempts total).` ,
261+ "" ,
262+ "Errors:" ,
263+ ...errorLines
264+ ] ;
265+
266+ if ( fileSections . length > 0 ) {
267+ lines . push ( "" , "Failing file contents:" , "" ) ;
268+ lines . push ( ...fileSections ) ;
269+ }
270+
271+ return lines . join ( "\n" ) ;
272+ }
273+
274+ /**
275+ * Formats a successful build response, including MPK path, warnings, and a
276+ * chaining instruction to call deploy-widget next.
277+ */
278+ export function formatBuildSuccessResponse (
279+ mpkPath : string | undefined ,
280+ widgetPath : string ,
281+ warnings : string [ ]
282+ ) : string {
283+ let message = "✅ Build successful!" ;
284+ if ( mpkPath ) {
285+ message += `\n\n📦 MPK output: ${ mpkPath } ` ;
286+ }
287+ if ( warnings . length > 0 ) {
288+ message += `\n\n⚠️ Warnings:\n${ warnings . map ( w => ` - ${ w } ` ) . join ( "\n" ) } ` ;
289+ }
290+ message += `\n\n🚀 Next step: Call deploy-widget with widgetPath: "${ widgetPath } " to copy the .mpk to your Mendix project's widgets/ directory.` ;
291+ return message ;
292+ }
293+
224294/**
225295 * Build progress phases for user-friendly messages.
226296 */
@@ -317,34 +387,6 @@ async function runBuild(widgetPath: string, tracker?: ProgressTracker): Promise<
317387 } ) ;
318388}
319389
320- /**
321- * Converts a parsed error to a structured error with suggestions.
322- */
323- function toStructuredError ( error : ParsedError ) : StructuredError {
324- const suggestions : Record < ParsedError [ "category" ] , string > = {
325- typescript :
326- "Check the TypeScript code at the specified location. Ensure props match the generated types from widget XML." ,
327- xml : "Verify your widget.xml follows the Mendix schema. Check property types and required attributes." ,
328- dependency :
329- "Run 'npm install' in the widget directory. If the issue persists, check that all dependencies are listed in package.json." ,
330- unknown : "Review the build output for more details. Try running 'npx pluggable-widget-tools build' manually."
331- } ;
332-
333- const codeMap : Record < ParsedError [ "category" ] , StructuredError [ "code" ] > = {
334- typescript : "ERR_BUILD_TS" ,
335- xml : "ERR_BUILD_XML" ,
336- dependency : "ERR_BUILD_MISSING_DEP" ,
337- unknown : "ERR_BUILD_UNKNOWN"
338- } ;
339-
340- return createStructuredError ( codeMap [ error . category ] , error . message , {
341- suggestion : suggestions [ error . category ] ,
342- file : error . file ,
343- line : error . line ,
344- column : error . column
345- } ) ;
346- }
347-
348390/**
349391 * Handler for the build-widget tool.
350392 */
@@ -400,42 +442,14 @@ async function handleBuildWidget(
400442 const mpkPath = result . mpkPath || findMpkFile ( widgetPath ) ;
401443
402444 if ( result . success ) {
403- let message = `✅ Build successful!` ;
404-
405- if ( mpkPath ) {
406- message += `\n\n📦 MPK output: ${ mpkPath } ` ;
407- }
408-
409- if ( result . warnings . length > 0 ) {
410- message += `\n\n⚠️ Warnings:\n${ result . warnings . map ( w => ` - ${ w } ` ) . join ( "\n" ) } ` ;
411- }
412-
413- return createToolResponse ( message ) ;
445+ return createToolResponse ( formatBuildSuccessResponse ( mpkPath , widgetPath , result . warnings ) ) ;
414446 } else {
415- // Return first error as structured error (most relevant)
416447 if ( result . errors . length > 0 ) {
417- const primaryError = toStructuredError ( result . errors [ 0 ] ) ;
418-
419- // Add additional errors to raw output if multiple
420- if ( result . errors . length > 1 ) {
421- const additionalErrors = result . errors
422- . slice ( 1 )
423- . map ( e => {
424- const loc = e . file ? `${ e . file } ${ e . line ? `:${ e . line } ` : "" } ` : "" ;
425- return loc ? `[${ loc } ] ${ e . message } ` : e . message ;
426- } )
427- . join ( "\n" ) ;
428-
429- primaryError . details = {
430- ...primaryError . details ,
431- rawOutput : `Additional errors (${ result . errors . length - 1 } ):\n${ additionalErrors } `
432- } ;
433- }
434-
435- return createStructuredErrorResponse ( primaryError ) ;
448+ const message = await formatBuildFailureResponse ( result . errors , widgetPath ) ;
449+ return { content : [ { type : "text" , text : message } ] , isError : true } ;
436450 }
437451
438- // Fallback for unknown failures
452+ // Fallback for unknown failures (no structured errors detected)
439453 return createStructuredErrorResponse (
440454 createStructuredError ( "ERR_BUILD_UNKNOWN" , "Build failed with unknown error" , {
441455 suggestion : "Check the raw build output for details." ,
@@ -459,7 +473,15 @@ export function registerBuildTools(server: McpServer, state: SessionState): void
459473 description :
460474 "Builds a Mendix pluggable widget using pluggable-widget-tools. " +
461475 "Validates XML, compiles TypeScript, generates types, and produces an .mpk file. " +
462- "Returns build errors if any, which can be used to fix issues." ,
476+ "If the build fails with TypeScript errors, the response includes ALL errors with " +
477+ "file locations AND the content of every failing source file. " +
478+ "RETRY LOOP: On failure, (1) read the errors and embedded file content, " +
479+ "(2) fix the TypeScript errors, (3) write the fixed files using write-widget-file, " +
480+ "(4) call build-widget again. Repeat until the build passes. " +
481+ "Maximum 3 total attempts — if still failing after 3 attempts, " +
482+ "report the errors and file contents to the user. " +
483+ "SUCCESS: When the build succeeds, you MUST call deploy-widget next with the same widgetPath " +
484+ "to copy the .mpk to the Mendix project. Do not stop after a successful build." ,
463485 inputSchema : buildWidgetSchema
464486 } ,
465487 ( args , context ) => handleBuildWidget ( args , context , state )
0 commit comments