Skip to content

Commit 8b2d10c

Browse files
committed
feat(mcp): implement MCP Sampling API and Prompts API with improved code structure
1 parent bd35c98 commit 8b2d10c

File tree

7 files changed

+1142
-131
lines changed

7 files changed

+1142
-131
lines changed

automation/mendix-widgets-copilot/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"scripts": {
1313
"build": "rollup -c && npm run clean",
1414
"build:tsc": "tsc",
15-
"clean": "rm -f build/*.d.ts build/*.d.ts.map build/diff-engine.js* build/guardrails.js* build/helpers.js* build/property-engine.js* build/types.js*",
15+
"clean": "rm -f build/*.d.ts build/*.d.ts.map build/diff-engine.js* build/guardrails.js* build/helpers.js* build/prompts.js* build/property-engine.js* build/sampling.js* build/types.js*",
1616
"dev": "tsx watch src/index.ts",
1717
"start": "node build/index.js"
1818
},

automation/mendix-widgets-copilot/src/helpers.ts

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,67 @@ export function extractMpkName(scripts: Record<string, string> = {}): string | u
1212
return undefined;
1313
}
1414

15+
export async function getWidgetName(packagePath: string): Promise<string | null> {
16+
try {
17+
const pkgRaw = await readFile(join(packagePath, "package.json"), "utf-8");
18+
const pkg = JSON.parse(pkgRaw);
19+
if (typeof pkg.widgetName === "string" && pkg.widgetName.trim()) {
20+
return pkg.widgetName.trim();
21+
}
22+
} catch {
23+
// ignore
24+
}
25+
const parts = packagePath.split("/");
26+
return parts[parts.length - 1].split("-")[0] || null;
27+
}
28+
29+
export async function resolveWidgetFiles(packagePath: string): Promise<{
30+
widgetName: string | null;
31+
srcPath: string;
32+
widgetXmlPath?: string;
33+
editorConfigPath?: string;
34+
editorPreviewPath?: string;
35+
}> {
36+
const srcPath = join(packagePath, "src");
37+
const widgetName = await getWidgetName(packagePath);
38+
let widgetXmlPath: string | undefined;
39+
let editorConfigPath: string | undefined;
40+
let editorPreviewPath: string | undefined;
41+
42+
try {
43+
const files = await readdir(srcPath);
44+
45+
if (widgetName) {
46+
const xmlCandidate = `${widgetName}.xml`;
47+
if (files.includes(xmlCandidate)) widgetXmlPath = join(srcPath, xmlCandidate);
48+
49+
const cfgCandidate = `${widgetName}.editorConfig.ts`;
50+
if (files.includes(cfgCandidate)) editorConfigPath = join(srcPath, cfgCandidate);
51+
52+
const prevCandidate = `${widgetName}.editorPreview.tsx`;
53+
if (files.includes(prevCandidate)) editorPreviewPath = join(srcPath, prevCandidate);
54+
}
55+
56+
// Fallbacks
57+
if (!widgetXmlPath) {
58+
const anyXml = files.find(f => f.endsWith(".xml") && !f.includes("package"));
59+
if (anyXml) widgetXmlPath = join(srcPath, anyXml);
60+
}
61+
if (!editorConfigPath) {
62+
const anyCfg = files.find(f => f.includes("editorConfig"));
63+
if (anyCfg) editorConfigPath = join(srcPath, anyCfg);
64+
}
65+
if (!editorPreviewPath) {
66+
const anyPrev = files.find(f => f.includes("editorPreview"));
67+
if (anyPrev) editorPreviewPath = join(srcPath, anyPrev);
68+
}
69+
} catch {
70+
// ignore; caller will handle errors
71+
}
72+
73+
return { widgetName, srcPath, widgetXmlPath, editorConfigPath, editorPreviewPath };
74+
}
75+
1576
export async function scanPackages(packagesDir: string): Promise<PackageInfo[]> {
1677
const packages: PackageInfo[] = [];
1778

@@ -102,16 +163,14 @@ export async function inspectWidget(packagePath: string): Promise<WidgetInspecti
102163
// Parse XML files
103164
const parser = new XMLParser({ ignoreAttributes: false });
104165

105-
// Look for widget XML in src/
166+
// Look for widget XML in src/ using central resolution
106167
const srcPath = join(packagePath, "src");
107168
try {
108-
const srcFiles = await readdir(srcPath);
109-
for (const file of srcFiles) {
110-
if (file.endsWith(".xml") && !file.includes("package")) {
111-
const xmlContent = await readFile(join(srcPath, file), "utf-8");
112-
inspection.widgetXml = parser.parse(xmlContent);
113-
break;
114-
}
169+
const resolved = await resolveWidgetFiles(packagePath);
170+
if (resolved.widgetXmlPath) {
171+
const xmlContent = await readFile(resolved.widgetXmlPath, "utf-8");
172+
inspection.widgetXml = parser.parse(xmlContent);
173+
inspection.widgetXmlPath = resolved.widgetXmlPath;
115174
}
116175
} catch (err) {
117176
inspection.errors.push(`Could not read src directory: ${err}`);
@@ -126,15 +185,11 @@ export async function inspectWidget(packagePath: string): Promise<WidgetInspecti
126185
inspection.errors.push(`Could not read package.xml: ${err}`);
127186
}
128187

129-
// Find editor config
188+
// Find editor config and preview based on central resolution
130189
try {
131-
const srcFiles = await readdir(srcPath);
132-
for (const file of srcFiles) {
133-
if (file.includes("editorConfig")) {
134-
inspection.editorConfig = join(srcPath, file);
135-
break;
136-
}
137-
}
190+
const resolved = await resolveWidgetFiles(packagePath);
191+
if (resolved.editorConfigPath) inspection.editorConfig = resolved.editorConfigPath;
192+
if (resolved.editorPreviewPath) inspection.editorPreview = resolved.editorPreviewPath;
138193
} catch (err) {
139194
inspection.errors.push(`Could not find editor config: ${err}`);
140195
}

automation/mendix-widgets-copilot/src/index.ts

Lines changed: 150 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
determineTestCommand,
2323
formatCommandResult
2424
} from "./helpers.js";
25+
import { getSampleForUri } from "./sampling.js";
26+
import { prompts } from "./prompts.js";
2527
import { Guardrails, withGuardrails } from "./guardrails.js";
2628
import { DiffEngine } from "./diff-engine.js";
2729
import { PropertyEngine } from "./property-engine.js";
@@ -66,14 +68,8 @@ async function main(): Promise<void> {
6668
version: "0.1.0",
6769
title: "Mendix Widgets Copilot",
6870
capabilities: {
69-
resources: {
70-
"mendix:repo": {
71-
description: "The Mendix Pluggable Widgets repository",
72-
icon: "https://www.mendix.com/favicon.ico",
73-
url: REPO_ROOT
74-
}
75-
},
76-
tools: {}
71+
resources: {}, // Resources will be registered dynamically
72+
tools: {} // Tools are registered below
7773
}
7874
});
7975

@@ -534,6 +530,9 @@ async function main(): Promise<void> {
534530
const diffResult = await diffEngine.createDiff(result.changes);
535531
const applyResult = await diffEngine.applyChanges(diffResult, { dryRun: false, createBackup: true });
536532

533+
// After applying, regenerate typings via pluggable-widgets-tools
534+
const regen = await propertyEngine.regenerateTypings(validatedPath);
535+
537536
return {
538537
content: [
539538
{
@@ -546,7 +545,8 @@ async function main(): Promise<void> {
546545
errors: applyResult.errors,
547546
rollbackToken: applyResult.rollbackInfo
548547
? JSON.stringify(applyResult.rollbackInfo)
549-
: undefined
548+
: undefined,
549+
typingsRegeneration: regen
550550
},
551551
null,
552552
2
@@ -617,6 +617,9 @@ async function main(): Promise<void> {
617617
const diffResult = await diffEngine.createDiff(result.changes);
618618
const applyResult = await diffEngine.applyChanges(diffResult, { dryRun: false, createBackup: true });
619619

620+
// After applying, regenerate typings via pluggable-widgets-tools
621+
const regen = await propertyEngine.regenerateTypings(validatedPath);
622+
620623
return {
621624
content: [
622625
{
@@ -629,7 +632,8 @@ async function main(): Promise<void> {
629632
errors: applyResult.errors,
630633
rollbackToken: applyResult.rollbackInfo
631634
? JSON.stringify(applyResult.rollbackInfo)
632-
: undefined
635+
: undefined,
636+
typingsRegeneration: regen
633637
},
634638
null,
635639
2
@@ -641,6 +645,142 @@ async function main(): Promise<void> {
641645
})
642646
);
643647

648+
// Register widget samples as resources for context
649+
// This provides a standard way for AI assistants to access widget context
650+
const packagesDir = join(REPO_ROOT, "packages");
651+
652+
// Register a resource for the repository overview
653+
server.registerResource(
654+
"repository-list",
655+
"mendix-widget://repository/list",
656+
{
657+
title: "Widget Repository Overview",
658+
description: "Complete list of widgets in the repository with metadata",
659+
mimeType: "application/json"
660+
},
661+
async uri => {
662+
const packages = await scanPackages(packagesDir);
663+
return {
664+
contents: [
665+
{
666+
uri: uri.href,
667+
text: JSON.stringify(
668+
{
669+
widget: "repository",
670+
type: "overview",
671+
timestamp: new Date().toISOString(),
672+
content: {
673+
metadata: {
674+
name: "Mendix Web Widgets Repository",
675+
version: "latest",
676+
path: REPO_ROOT,
677+
description: "Complete list of widgets in the repository"
678+
},
679+
widgets: packages
680+
}
681+
},
682+
null,
683+
2
684+
),
685+
mimeType: "application/json"
686+
}
687+
]
688+
};
689+
}
690+
);
691+
692+
// Register dynamic resources for widget contexts
693+
// We'll register a few key widgets as examples - in production, you might want to register all
694+
const registerWidgetResources = async () => {
695+
const packages = await scanPackages(packagesDir);
696+
697+
// Register resources for the first 10 widgets as examples
698+
const widgetsToRegister = packages.filter(pkg => pkg.kind === "pluggableWidget").slice(0, 10);
699+
700+
for (const widget of widgetsToRegister) {
701+
const widgetName = widget.name.replace("@mendix/", "");
702+
703+
// Register overview resource
704+
server.registerResource(
705+
`${widgetName}-overview`,
706+
`mendix-widget://${widgetName}/overview`,
707+
{
708+
title: `${widgetName} - Complete Overview`,
709+
description: `Full context for ${widgetName} widget including manifest, types, and configuration`,
710+
mimeType: "application/json"
711+
},
712+
async uri => {
713+
const sampleContent = await getSampleForUri(uri.href, REPO_ROOT);
714+
if ("error" in sampleContent) {
715+
throw new Error(sampleContent.error);
716+
}
717+
return {
718+
contents: [
719+
{
720+
uri: uri.href,
721+
text: JSON.stringify(sampleContent, null, 2),
722+
mimeType: "application/json"
723+
}
724+
]
725+
};
726+
}
727+
);
728+
729+
// Register properties resource
730+
server.registerResource(
731+
`${widgetName}-properties`,
732+
`mendix-widget://${widgetName}/properties`,
733+
{
734+
title: `${widgetName} - Properties`,
735+
description: `Property definitions and structure for ${widgetName} widget`,
736+
mimeType: "application/json"
737+
},
738+
async uri => {
739+
const sampleContent = await getSampleForUri(uri.href, REPO_ROOT);
740+
if ("error" in sampleContent) {
741+
throw new Error(sampleContent.error);
742+
}
743+
return {
744+
contents: [
745+
{
746+
uri: uri.href,
747+
text: JSON.stringify(sampleContent, null, 2),
748+
mimeType: "application/json"
749+
}
750+
]
751+
};
752+
}
753+
);
754+
}
755+
};
756+
757+
// Register widget resources on startup
758+
await registerWidgetResources();
759+
760+
// Register MCP Prompts for guided workflows
761+
for (const prompt of prompts) {
762+
// Build the argsSchema object from prompt arguments
763+
const argsSchema: Record<string, any> = {};
764+
for (const arg of prompt.arguments) {
765+
argsSchema[arg.name] = arg.schema || z.string();
766+
if (!arg.required) {
767+
argsSchema[arg.name] = argsSchema[arg.name].optional();
768+
}
769+
}
770+
771+
server.registerPrompt(
772+
prompt.name,
773+
{
774+
title: prompt.title,
775+
description: prompt.description,
776+
argsSchema: argsSchema
777+
},
778+
prompt.handler
779+
);
780+
}
781+
782+
console.error(`Registered ${prompts.length} prompt templates for guided workflows`);
783+
644784
await server.connect(new StdioServerTransport());
645785
}
646786

0 commit comments

Comments
 (0)