Skip to content

Commit f878d6c

Browse files
committed
refactor(pluggable-widgets-mcp): fix prompt timeout, refactor schema, add custom dir
1 parent 4eee681 commit f878d6c

4 files changed

Lines changed: 208 additions & 99 deletions

File tree

packages/pluggable-widgets-mcp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@
3737
"keywords": [],
3838
"packageManager": "pnpm@10.17.0",
3939
"engines": {
40-
"node": ">=22"
40+
"node": ">=20"
4141
}
4242
}

packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts

Lines changed: 33 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,27 @@
11
import { mkdir } from "node:fs/promises";
22
import { z } from "zod";
33
import { GENERATIONS_DIR } from "@/config";
4-
import type { ToolContext, ToolDefinition, ToolResponse } from "@/tools/types";
54
import {
6-
buildWidgetOptions,
75
DEFAULT_WIDGET_OPTIONS,
8-
GENERATOR_PROMPTS,
9-
runWidgetGenerator,
10-
SCAFFOLD_PROGRESS
11-
} from "@/tools/utils/generator";
6+
widgetOptionsSchema,
7+
type ToolContext,
8+
type ToolDefinition,
9+
type ToolResponse
10+
} from "@/tools/types";
11+
import { buildWidgetOptions, GENERATOR_PROMPTS, runWidgetGenerator, SCAFFOLD_PROGRESS } from "@/tools/utils/generator";
1212
import { ProgressTracker } from "@/tools/utils/progress-tracker";
1313
import { createErrorResponse, createToolResponse } from "@/tools/utils/response";
1414

15-
const createWidgetSchema = z.object({
16-
name: z
15+
/**
16+
* Schema for create-widget tool input.
17+
* Extends the base widgetOptionsSchema with tool-specific options like outputPath.
18+
*/
19+
const createWidgetSchema = widgetOptionsSchema.extend({
20+
outputPath: z
1721
.string()
18-
.min(1)
19-
.max(100)
20-
.describe("[REQUIRED] The name of the widget in PascalCase (e.g., 'MyAwesomeWidget', 'DataChart')"),
21-
description: z.string().min(1).max(200).describe("[REQUIRED] A brief description of what the widget does"),
22-
version: z
23-
.string()
24-
.regex(/^\d+\.\d+\.\d+$/, "Version must be in semver format: x.y.z")
25-
.optional()
26-
.describe(`[OPTIONAL] Initial version in semver format. Default: "${DEFAULT_WIDGET_OPTIONS.version}"`),
27-
author: z
28-
.string()
29-
.min(1)
30-
.max(100)
31-
.optional()
32-
.describe(`[OPTIONAL] Author name. Default: "${DEFAULT_WIDGET_OPTIONS.author}"`),
33-
license: z
34-
.string()
35-
.min(1)
36-
.max(50)
37-
.optional()
38-
.describe(`[OPTIONAL] License type. Default: "${DEFAULT_WIDGET_OPTIONS.license}"`),
39-
organization: z
40-
.string()
41-
.min(1)
42-
.max(100)
43-
.optional()
44-
.describe(
45-
`[OPTIONAL] Organization name for the widget namespace. Default: "${DEFAULT_WIDGET_OPTIONS.organization}"`
46-
),
47-
template: z
48-
.enum(["full", "empty"])
49-
.optional()
50-
.describe(
51-
`[OPTIONAL] Widget template: "full" includes sample code and examples, "empty" is minimal/blank. Default: "${DEFAULT_WIDGET_OPTIONS.template}"`
52-
),
53-
programmingLanguage: z
54-
.enum(["typescript", "javascript"])
55-
.optional()
56-
.describe(
57-
`[OPTIONAL] Programming language for the widget source code. Default: "${DEFAULT_WIDGET_OPTIONS.programmingLanguage}"`
58-
),
59-
unitTests: z
60-
.boolean()
61-
.optional()
62-
.describe(`[OPTIONAL] Include unit test setup with Jest. Default: ${DEFAULT_WIDGET_OPTIONS.unitTests}`),
63-
e2eTests: z
64-
.boolean()
6522
.optional()
6623
.describe(
67-
`[OPTIONAL] Include end-to-end test setup with Playwright. Default: ${DEFAULT_WIDGET_OPTIONS.e2eTests}`
24+
"[OPTIONAL] Directory where widget will be created. Defaults to ./generations/ within the MCP server package."
6825
)
6926
});
7027

@@ -87,9 +44,22 @@ OPTIONAL (with defaults):
8744
• programmingLanguage: "typescript" or "javascript" (default: "${DEFAULT_WIDGET_OPTIONS.programmingLanguage}")
8845
• unitTests: Include Jest test setup (default: ${DEFAULT_WIDGET_OPTIONS.unitTests})
8946
• e2eTests: Include Playwright E2E tests (default: ${DEFAULT_WIDGET_OPTIONS.e2eTests})
47+
• outputPath: Directory where widget will be created (default: ./generations/)
9048
9149
Ask the user if they want to customize any options before proceeding.`;
9250

51+
/**
52+
* Returns scaffolding-related tools for widget creation and management.
53+
*
54+
* Currently contains only the create-widget tool, but structured as an array
55+
* for extensibility. This modular pattern allows easy addition of related tools
56+
* such as:
57+
* - Widget property editing
58+
* - XML configuration management
59+
* - Build and deployment automation
60+
*
61+
* @see AGENTS.md Roadmap Context section for planned additions
62+
*/
9363
export function getScaffoldingTools(): Array<ToolDefinition<CreateWidgetInput>> {
9464
return [
9565
{
@@ -104,6 +74,7 @@ export function getScaffoldingTools(): Array<ToolDefinition<CreateWidgetInput>>
10474

10575
async function handleCreateWidget(args: CreateWidgetInput, context: ToolContext): Promise<ToolResponse> {
10676
const options = buildWidgetOptions(args);
77+
const outputDir = args.outputPath ?? GENERATIONS_DIR;
10778
const tracker = new ProgressTracker({
10879
context,
10980
logger: "scaffolding",
@@ -116,14 +87,15 @@ async function handleCreateWidget(args: CreateWidgetInput, context: ToolContext)
11687
await tracker.info(`Starting widget scaffolding for "${options.name}"...`, {
11788
widgetName: options.name,
11889
template: options.template,
119-
organization: options.organization
90+
organization: options.organization,
91+
outputDir
12092
});
12193

122-
// Ensure generations directory exists
123-
await mkdir(GENERATIONS_DIR, { recursive: true });
94+
// Ensure output directory exists
95+
await mkdir(outputDir, { recursive: true });
12496

125-
const widgetFolder = await runWidgetGenerator(options, tracker);
126-
const widgetPath = `${GENERATIONS_DIR}/${widgetFolder}`;
97+
const widgetFolder = await runWidgetGenerator(options, tracker, outputDir);
98+
const widgetPath = `${outputDir}/${widgetFolder}`;
12799

128100
console.error(`[create-widget] Widget created successfully at ${widgetPath}`);
129101
await tracker.progress(SCAFFOLD_PROGRESS.COMPLETE, "Widget created successfully!");

packages/pluggable-widgets-mcp/src/tools/types.ts

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
22
import type { ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js";
3-
import type { ZodType } from "zod";
3+
import { z, type ZodType } from "zod";
44

55
// =============================================================================
66
// MCP Core Types
@@ -53,17 +53,99 @@ export type LogLevel = "debug" | "info" | "notice" | "warning" | "error";
5353
// =============================================================================
5454

5555
/**
56-
* Options for creating a new Mendix pluggable widget.
56+
* Default values for widget options.
57+
* Centralized here to be used by both schema descriptions and buildWidgetOptions().
58+
*/
59+
export const DEFAULT_WIDGET_OPTIONS = {
60+
version: "1.0.0",
61+
author: "Mendix",
62+
license: "Apache-2.0",
63+
organization: "Mendix",
64+
template: "empty" as const,
65+
programmingLanguage: "typescript" as const,
66+
unitTests: true,
67+
e2eTests: false
68+
} as const;
69+
70+
/**
71+
* Zod schema for widget creation options.
72+
* Single source of truth for widget options - type is derived via z.infer.
73+
*/
74+
export const widgetOptionsSchema = z.object({
75+
name: z
76+
.string()
77+
.min(1)
78+
.max(100)
79+
.describe("[REQUIRED] The name of the widget in PascalCase (e.g., 'MyAwesomeWidget', 'DataChart')"),
80+
description: z.string().min(1).max(200).describe("[REQUIRED] A brief description of what the widget does"),
81+
version: z
82+
.string()
83+
.regex(/^\d+\.\d+\.\d+$/, "Version must be in semver format: x.y.z")
84+
.optional()
85+
.describe(`[OPTIONAL] Initial version in semver format. Default: "${DEFAULT_WIDGET_OPTIONS.version}"`),
86+
author: z
87+
.string()
88+
.min(1)
89+
.max(100)
90+
.optional()
91+
.describe(`[OPTIONAL] Author name. Default: "${DEFAULT_WIDGET_OPTIONS.author}"`),
92+
license: z
93+
.string()
94+
.min(1)
95+
.max(50)
96+
.optional()
97+
.describe(`[OPTIONAL] License type. Default: "${DEFAULT_WIDGET_OPTIONS.license}"`),
98+
organization: z
99+
.string()
100+
.min(1)
101+
.max(100)
102+
.optional()
103+
.describe(
104+
`[OPTIONAL] Organization name for the widget namespace. Default: "${DEFAULT_WIDGET_OPTIONS.organization}"`
105+
),
106+
template: z
107+
.enum(["full", "empty"])
108+
.optional()
109+
.describe(
110+
`[OPTIONAL] Widget template: "full" includes sample code and examples, "empty" is minimal/blank. Default: "${DEFAULT_WIDGET_OPTIONS.template}"`
111+
),
112+
programmingLanguage: z
113+
.enum(["typescript", "javascript"])
114+
.optional()
115+
.describe(
116+
`[OPTIONAL] Programming language for the widget source code. Default: "${DEFAULT_WIDGET_OPTIONS.programmingLanguage}"`
117+
),
118+
unitTests: z
119+
.boolean()
120+
.optional()
121+
.describe(`[OPTIONAL] Include unit test setup with Jest. Default: ${DEFAULT_WIDGET_OPTIONS.unitTests}`),
122+
e2eTests: z
123+
.boolean()
124+
.optional()
125+
.describe(
126+
`[OPTIONAL] Include end-to-end test setup with Playwright. Default: ${DEFAULT_WIDGET_OPTIONS.e2eTests}`
127+
)
128+
});
129+
130+
/**
131+
* Input options for creating a new Mendix pluggable widget (with optional fields).
132+
* Derived from widgetOptionsSchema to ensure type-schema consistency.
133+
*/
134+
export type WidgetOptionsInput = z.infer<typeof widgetOptionsSchema>;
135+
136+
/**
137+
* Resolved widget options with all defaults applied (all fields required).
138+
* This is the type returned by buildWidgetOptions() after applying defaults.
57139
*/
58140
export interface WidgetOptions {
59141
name: string;
60142
description: string;
61143
version: string;
62144
author: string;
63145
license: string;
64-
organization?: string;
65-
template?: "full" | "empty";
66-
programmingLanguage?: "typescript" | "javascript";
67-
unitTests?: boolean;
68-
e2eTests?: boolean;
146+
organization: string;
147+
template: "full" | "empty";
148+
programmingLanguage: "typescript" | "javascript";
149+
unitTests: boolean;
150+
e2eTests: boolean;
69151
}

0 commit comments

Comments
 (0)