aui-mcp-server 0.0.1

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.
Files changed (62) hide show
  1. package/README.md +122 -0
  2. package/dist/cli.cjs +1088 -0
  3. package/dist/cli.cjs.map +1 -0
  4. package/dist/cli.d.cts +1 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +1076 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/client/index.cjs +619 -0
  9. package/dist/client/index.cjs.map +1 -0
  10. package/dist/client/index.d.cts +194 -0
  11. package/dist/client/index.d.ts +194 -0
  12. package/dist/client/index.js +584 -0
  13. package/dist/client/index.js.map +1 -0
  14. package/dist/index.cjs +1053 -0
  15. package/dist/index.cjs.map +1 -0
  16. package/dist/index.d.cts +163 -0
  17. package/dist/index.d.ts +163 -0
  18. package/dist/index.js +1036 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/rsbuild.cjs +1049 -0
  21. package/dist/rsbuild.cjs.map +1 -0
  22. package/dist/rsbuild.d.cts +12 -0
  23. package/dist/rsbuild.d.ts +12 -0
  24. package/dist/rsbuild.js +1038 -0
  25. package/dist/rsbuild.js.map +1 -0
  26. package/dist/rspack.cjs +1016 -0
  27. package/dist/rspack.cjs.map +1 -0
  28. package/dist/rspack.d.cts +40 -0
  29. package/dist/rspack.d.ts +40 -0
  30. package/dist/rspack.js +1005 -0
  31. package/dist/rspack.js.map +1 -0
  32. package/dist/server.cjs +304 -0
  33. package/dist/server.cjs.map +1 -0
  34. package/dist/server.d.cts +16 -0
  35. package/dist/server.d.ts +16 -0
  36. package/dist/server.js +297 -0
  37. package/dist/server.js.map +1 -0
  38. package/package.json +72 -0
  39. package/src/catalog/build.ts +89 -0
  40. package/src/catalog/entry.ts +183 -0
  41. package/src/catalog/parser.ts +173 -0
  42. package/src/catalog/tool_parser.ts +145 -0
  43. package/src/cli.ts +318 -0
  44. package/src/client/handshake.ts +166 -0
  45. package/src/client/index.ts +6 -0
  46. package/src/client/registry.tsx +184 -0
  47. package/src/client/types.ts +136 -0
  48. package/src/client/useA2UIStream.ts +378 -0
  49. package/src/client/useLogger.ts +147 -0
  50. package/src/generator.ts +100 -0
  51. package/src/index.ts +17 -0
  52. package/src/mcp-app-poc.html +69 -0
  53. package/src/poc.ts +88 -0
  54. package/src/rsbuild.ts +46 -0
  55. package/src/rspack.ts +282 -0
  56. package/src/server.ts +391 -0
  57. package/src/templates.ts +51 -0
  58. package/src/types.ts +195 -0
  59. package/src/utils.ts +29 -0
  60. package/test.js +16 -0
  61. package/tsconfig.json +19 -0
  62. package/tsup.config.ts +27 -0
package/src/cli.ts ADDED
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+
7
+ import chokidar from 'chokidar';
8
+
9
+ import { buildCatalogFromStats } from './catalog/build';
10
+ import { generateAuiMcpAssets } from './generator';
11
+ import type { AuiXCatalog, MfStats } from './types';
12
+ import { serveAuiMcpServer, type TransportType } from './server';
13
+
14
+ function getArg(flag: string): string | undefined {
15
+ const idx = process.argv.indexOf(flag);
16
+ if (idx === -1) return undefined;
17
+ return process.argv[idx + 1];
18
+ }
19
+
20
+ function hasFlag(flag: string): boolean {
21
+ return process.argv.includes(flag);
22
+ }
23
+
24
+ function getTransportType(): TransportType {
25
+ const v = getArg('--transport') ?? process.env['AUI_MCP_TRANSPORT'];
26
+ return v === 'sse' ? 'sse' : 'stdio';
27
+ }
28
+
29
+ function getPort(): number {
30
+ const v = getArg('--port') ?? process.env['AUI_MCP_PORT'];
31
+ return v ? Number(v) : 8001;
32
+ }
33
+
34
+ function printHelp(): void {
35
+ console.log(`aui-mcp (from aui-mcp-server)
36
+
37
+ Commands:
38
+ aui-mcp catalog <mf-stats...> Generate aui-x-catalog.json
39
+ aui-mcp generate [options] Generate dist/mcp assets
40
+ aui-mcp serve [options] Start MCP server (stdio or SSE)
41
+ aui-mcp dev [options] Generate then start server (watch)
42
+
43
+ Catalog options:
44
+ -o, --output <path> Output file (default: aui-x-catalog.json)
45
+ --dry-run Print output to stdout
46
+ -v, --verbose Verbose logs
47
+
48
+ Common options:
49
+ --catalog <path> Path to aui-x-catalog.json (default: aui-x-catalog.json)
50
+ --outDir <dir> Remote dist directory (default: dist)
51
+ --mcpDirname <name> Subdir under outDir (default: mcp)
52
+
53
+ Generate options:
54
+ --noServer Only emit manifest.json
55
+
56
+ Serve options:
57
+ --manifest <path> Path to manifest.json (default: dist/mcp/manifest.json)
58
+ --watch Enable hot reload (manifest/catalog)
59
+ --transport <type> stdio|sse (default: stdio; env: AUI_MCP_TRANSPORT)
60
+ --port <number> SSE port (default: 8001; env: AUI_MCP_PORT)
61
+
62
+ Dev options:
63
+ --noAutoGenerate Do not re-generate when catalog changes
64
+ `);
65
+ }
66
+
67
+ function readCompiledTypesFromDist(distDir: string, componentName: string): string | undefined {
68
+ const dtsPath = path.join(distDir, '@mf-types', 'compiled-types', `${componentName}.d.ts`);
69
+ if (!fs.existsSync(dtsPath)) return undefined;
70
+ return fs.readFileSync(dtsPath, 'utf-8');
71
+ }
72
+
73
+ interface CatalogCliOptions {
74
+ statsFiles: string[];
75
+ output: string;
76
+ dryRun: boolean;
77
+ verbose: boolean;
78
+ }
79
+
80
+ function parseCatalogArgs(argv: string[]): CatalogCliOptions {
81
+ const args = argv.slice(3);
82
+ const opts: CatalogCliOptions = {
83
+ statsFiles: [],
84
+ output: 'aui-x-catalog.json',
85
+ dryRun: false,
86
+ verbose: false,
87
+ };
88
+
89
+ for (let i = 0; i < args.length; i++) {
90
+ const a = args[i];
91
+ if (!a) continue;
92
+
93
+ if (a === '-o' || a === '--output') {
94
+ opts.output = args[++i] ?? opts.output;
95
+ continue;
96
+ }
97
+
98
+ if (a === '--dry-run') {
99
+ opts.dryRun = true;
100
+ continue;
101
+ }
102
+
103
+ if (a === '--verbose' || a === '-v') {
104
+ opts.verbose = true;
105
+ continue;
106
+ }
107
+
108
+ if (!a.startsWith('-')) {
109
+ opts.statsFiles.push(a);
110
+ }
111
+ }
112
+
113
+ return opts;
114
+ }
115
+
116
+ async function cmdCatalog(): Promise<void> {
117
+ const opts = parseCatalogArgs(process.argv);
118
+
119
+ if (opts.statsFiles.length === 0) {
120
+ console.error('Usage: aui-mcp catalog [options] <mf-stats.json...>');
121
+ console.error('Example: aui-mcp catalog ./dist/mf-stats.json -o ./aui-x-catalog.json');
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+
126
+ const catalog: AuiXCatalog = {};
127
+ let hasErrors = false;
128
+
129
+ for (const statsFile of opts.statsFiles) {
130
+ const absFile = path.resolve(process.cwd(), statsFile);
131
+
132
+ if (!fs.existsSync(absFile)) {
133
+ console.error(`[aui-mcp] Cannot find mf-stats.json: ${absFile}`);
134
+ hasErrors = true;
135
+ continue;
136
+ }
137
+
138
+ let stats: MfStats;
139
+ try {
140
+ stats = JSON.parse(fs.readFileSync(absFile, 'utf-8')) as MfStats;
141
+ } catch (e) {
142
+ console.error(`[aui-mcp] Failed to parse mf-stats.json: ${absFile}`);
143
+ console.error(e);
144
+ hasErrors = true;
145
+ continue;
146
+ }
147
+
148
+ const distDir = path.dirname(absFile);
149
+
150
+ const result = buildCatalogFromStats({
151
+ stats,
152
+ readCompiledTypes: (componentName) => readCompiledTypesFromDist(distDir, componentName),
153
+ });
154
+
155
+ for (const [name, entry] of Object.entries(result.catalog)) {
156
+ catalog[name] = entry;
157
+ }
158
+
159
+ for (const issue of result.issues) {
160
+ console.warn(
161
+ `[aui-mcp] ⚠ ${issue.componentName ?? 'unknown'}: ${issue.message}${issue.detail ? ` (${issue.detail})` : ''}`
162
+ );
163
+ hasErrors = true;
164
+ }
165
+
166
+ if (result.missingJsdocComponents.length > 0) {
167
+ const names = result.missingJsdocComponents.join(', ');
168
+ console.warn(
169
+ `[aui-mcp] ⚠ Missing @aui-component JSDoc: ${names}. Please run skills/aui-jsdoc-gen to write back to source files and retry.`
170
+ );
171
+ hasErrors = true;
172
+ }
173
+ }
174
+
175
+ if (Object.keys(catalog).length === 0) {
176
+ console.error('[aui-mcp] No catalog entries were generated successfully.');
177
+ process.exitCode = 1;
178
+ return;
179
+ }
180
+
181
+ const outputJson = JSON.stringify(catalog, null, 2) + '\n';
182
+
183
+ if (opts.dryRun) {
184
+ console.log(outputJson);
185
+ return;
186
+ }
187
+
188
+ const outPath = path.resolve(process.cwd(), opts.output);
189
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
190
+ fs.writeFileSync(outPath, outputJson, 'utf-8');
191
+ console.log(`[aui-mcp] Written: ${outPath}`);
192
+
193
+ if (hasErrors) {
194
+ console.warn(
195
+ '[aui-mcp] Some components failed to generate catalog entries (see warnings above), but partial output has been written.'
196
+ );
197
+ }
198
+ }
199
+
200
+ async function cmdGenerate(): Promise<void> {
201
+ const outDir = getArg('--outDir') ?? 'dist';
202
+ const catalogPath = getArg('--catalog') ?? 'aui-x-catalog.json';
203
+ const mcpDirname = getArg('--mcpDirname');
204
+ const emitServer = !hasFlag('--noServer');
205
+
206
+ const result = await generateAuiMcpAssets({
207
+ outDir,
208
+ catalogPath: path.resolve(process.cwd(), catalogPath),
209
+ mcpDirname,
210
+ emitServer,
211
+ });
212
+
213
+ console.log(`Generated: ${result.manifestPath}`);
214
+ if (result.serverPath) {
215
+ console.log(`Generated: ${result.serverPath}`);
216
+ }
217
+
218
+ // dev shortcut: generate then serve
219
+ if (hasFlag('--dev')) {
220
+ const manifestPath = result.manifestPath;
221
+ await serveAuiMcpServer({
222
+ manifestPath,
223
+ watch: true,
224
+ transport: getTransportType(),
225
+ port: getPort(),
226
+ });
227
+ }
228
+ }
229
+
230
+ async function cmdServe(): Promise<void> {
231
+ const outDir = getArg('--outDir') ?? 'dist';
232
+ const mcpDirname = getArg('--mcpDirname') ?? 'mcp';
233
+
234
+ const catalogPath = getArg('--catalog') ?? 'aui-x-catalog.json';
235
+ const manifestPath =
236
+ getArg('--manifest') ?? path.join(outDir, mcpDirname, 'manifest.json');
237
+
238
+ // If user explicitly sets --catalog, use it.
239
+ // Otherwise default to manifest.json if it exists.
240
+ const useCatalog = hasFlag('--catalog');
241
+
242
+ await serveAuiMcpServer({
243
+ catalogPath: useCatalog ? path.resolve(process.cwd(), catalogPath) : undefined,
244
+ manifestPath: useCatalog ? undefined : path.resolve(process.cwd(), manifestPath),
245
+ watch: hasFlag('--watch') || Boolean(process.env['AUI_MCP_WATCH']),
246
+ transport: getTransportType(),
247
+ port: getPort(),
248
+ });
249
+ }
250
+
251
+ async function cmdDev(): Promise<void> {
252
+ const outDir = getArg('--outDir') ?? 'dist';
253
+ const catalogPath = path.resolve(process.cwd(), getArg('--catalog') ?? 'aui-x-catalog.json');
254
+ const mcpDirname = getArg('--mcpDirname') ?? 'mcp';
255
+
256
+ const enableAutoGenerate = !hasFlag('--noAutoGenerate');
257
+
258
+ const result = await generateAuiMcpAssets({ outDir, catalogPath, mcpDirname, emitServer: true });
259
+
260
+ if (enableAutoGenerate) {
261
+ chokidar
262
+ .watch(catalogPath, { ignoreInitial: true })
263
+ .on('change', async () => {
264
+ try {
265
+ await generateAuiMcpAssets({ outDir, catalogPath, mcpDirname, emitServer: true });
266
+ // eslint-disable-next-line no-console
267
+ console.error(`[aui-mcp] regenerated from ${catalogPath}`);
268
+ } catch (err) {
269
+ // eslint-disable-next-line no-console
270
+ console.error('[aui-mcp] regenerate failed:', err);
271
+ }
272
+ });
273
+ }
274
+
275
+ await serveAuiMcpServer({
276
+ manifestPath: result.manifestPath,
277
+ watch: true,
278
+ transport: getTransportType(),
279
+ port: getPort(),
280
+ });
281
+ }
282
+
283
+ async function main(): Promise<void> {
284
+ const cmd = process.argv[2];
285
+ if (!cmd || cmd === '--help' || cmd === '-h') {
286
+ printHelp();
287
+ return;
288
+ }
289
+
290
+ if (cmd === 'catalog') {
291
+ await cmdCatalog();
292
+ return;
293
+ }
294
+
295
+ if (cmd === 'generate') {
296
+ await cmdGenerate();
297
+ return;
298
+ }
299
+
300
+ if (cmd === 'serve') {
301
+ await cmdServe();
302
+ return;
303
+ }
304
+
305
+ if (cmd === 'dev') {
306
+ await cmdDev();
307
+ return;
308
+ }
309
+
310
+ console.error(`Unknown command: ${cmd}`);
311
+ printHelp();
312
+ process.exitCode = 1;
313
+ }
314
+
315
+ main().catch((err) => {
316
+ console.error(err);
317
+ process.exitCode = 1;
318
+ });
@@ -0,0 +1,166 @@
1
+ import type { ComponentRegistry } from '@a2ui/react';
2
+ import type { ServerToClientMessage } from '@a2ui/react';
3
+ import { logger } from './useLogger';
4
+ import { createMFRegistry, syncRegistryFromCatalog } from './registry';
5
+ import type { ToolCallPayload } from './types';
6
+ import { A2UI_EXTENSION_URI } from './types';
7
+
8
+ interface A2UIValue {
9
+ literalString?: string;
10
+ path?: string;
11
+ }
12
+
13
+ /** Convert any input into an A2UI literalString value object. */
14
+ function toLiteralStringValue(value: unknown): A2UIValue | undefined {
15
+ if (value === undefined || value === null) return undefined;
16
+
17
+ if (typeof value === 'string') {
18
+ return { literalString: value };
19
+ }
20
+
21
+ if (typeof value === 'object') {
22
+ const obj = value as Partial<A2UIValue>;
23
+ if (typeof obj.literalString === 'string' || typeof obj.path === 'string') {
24
+ return {
25
+ ...(obj.literalString ? { literalString: obj.literalString } : {}),
26
+ ...(obj.path ? { path: obj.path } : {}),
27
+ };
28
+ }
29
+ }
30
+
31
+ return { literalString: String(value) };
32
+ }
33
+
34
+ /**
35
+ * A2A handshake:
36
+ * - Fetch AgentCard from `{agentUrl}/agent-card`
37
+ * - Parse `inlineCatalogs` from `capabilities.extensions`
38
+ * - Auto-register MF components via `syncRegistryFromCatalog()`
39
+ */
40
+ export async function performHandshake(
41
+ agentUrl: string,
42
+ registry?: ComponentRegistry,
43
+ ): Promise<Record<string, unknown>> {
44
+ try {
45
+ const base = agentUrl.replace(/\/$/, '');
46
+ const handshakeUrl = base.endsWith('/agent-card') ? base : `${base}/agent-card`;
47
+
48
+ const response = await fetch(handshakeUrl);
49
+ if (!response.ok) {
50
+ throw new Error(`Failed to fetch AgentCard from ${handshakeUrl}: ${response.statusText}`);
51
+ }
52
+
53
+ const agentCard = await response.json();
54
+
55
+ const a2uiExt = agentCard.capabilities?.extensions?.find(
56
+ (ext: any) => ext.uri === A2UI_EXTENSION_URI,
57
+ );
58
+
59
+ if (!a2uiExt || !a2uiExt.params?.inlineCatalogs) {
60
+ logger.warn('[AUI-X] No inlineCatalogs found in AgentCard. Handshake skipped.');
61
+ return {};
62
+ }
63
+
64
+ const catalogs = a2uiExt.params.inlineCatalogs as Array<{
65
+ id?: string;
66
+ components?: Record<string, unknown>;
67
+ }>;
68
+
69
+ const allComponents: Record<string, unknown> = {};
70
+ const effectiveRegistry = registry ?? createMFRegistry();
71
+
72
+ catalogs.forEach((cat) => {
73
+ if (!cat.components) return;
74
+ const count = Object.keys(cat.components).length;
75
+ logger.info(
76
+ `[AUI-X] Found catalog "${cat.id ?? 'inline'}", syncing ${count} components from handshake`,
77
+ );
78
+ Object.assign(allComponents, cat.components);
79
+ syncRegistryFromCatalog(effectiveRegistry, cat.components);
80
+ });
81
+
82
+ return allComponents;
83
+ } catch (error) {
84
+ logger.error('[AUI-X] Handshake error:', error);
85
+ return {};
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Generic renderer: map a tool call to A2UI messages based on the Catalog schema.
91
+ */
92
+ export function render(
93
+ toolCall: ToolCallPayload,
94
+ catalog: Record<string, any>,
95
+ ): ServerToClientMessage[] {
96
+ const { tool_name, tool_input } = toolCall;
97
+
98
+ // 1) Derive component type
99
+ let componentType = tool_name;
100
+ if (tool_name.startsWith('render_')) {
101
+ const parts = tool_name.split('_').slice(1);
102
+ componentType = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
103
+ }
104
+
105
+ const schema = catalog[componentType];
106
+ if (!schema) {
107
+ throw new Error(
108
+ `[AUI-X] Component type "${componentType}" not found in catalog for tool "${tool_name}"`,
109
+ );
110
+ }
111
+
112
+ const surfaceId = `${tool_name}-surface-handshake`;
113
+ const rootId = 'root-column';
114
+
115
+ // 2) Map props
116
+ const props: Record<string, unknown> = {};
117
+ if (tool_input) {
118
+ Object.entries(tool_input).forEach(([key, value]) => {
119
+ if (key === 'mfData' || key === 'dataModel') {
120
+ props[key] = value;
121
+ } else {
122
+ const val = toLiteralStringValue(value);
123
+ if (val) props[key] = val;
124
+ }
125
+ });
126
+ }
127
+
128
+ // 3) Build rendering instructions
129
+ const components = [
130
+ {
131
+ id: rootId,
132
+ component: {
133
+ Column: {
134
+ children: {
135
+ explicitList: ['dynamic-card'],
136
+ },
137
+ },
138
+ },
139
+ },
140
+ {
141
+ id: 'dynamic-card',
142
+ component: {
143
+ [componentType]: props,
144
+ },
145
+ },
146
+ ];
147
+
148
+ return [
149
+ {
150
+ beginRendering: {
151
+ surfaceId,
152
+ root: rootId,
153
+ styles: {
154
+ primaryColor: '#1DB954',
155
+ font: 'Roboto',
156
+ },
157
+ },
158
+ },
159
+ {
160
+ surfaceUpdate: {
161
+ surfaceId,
162
+ components,
163
+ },
164
+ },
165
+ ];
166
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './handshake';
3
+ export * from './registry';
4
+ export * from './useA2UIStream';
5
+ export * from './useLogger';
6
+
@@ -0,0 +1,184 @@
1
+ import React, { lazy } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import { createInstance } from '@module-federation/enhanced/runtime';
4
+ import * as A2UI from '@a2ui/react';
5
+ import {
6
+ ComponentRegistry,
7
+ registerDefaultCatalog,
8
+ type ComponentRegistration,
9
+ } from '@a2ui/react';
10
+ import type { CreateMFRegistryOptions, XLoaderConfig } from './types';
11
+ import { logger } from './useLogger';
12
+
13
+ // ── Inline ErrorPlaceholder (avoid depending on sample app components) ──────
14
+
15
+ const ErrorPlaceholder: React.FC<{ title?: string; message?: string }> = ({
16
+ title = 'Component load failed',
17
+ message,
18
+ }) => (
19
+ <div
20
+ style={{
21
+ padding: '0.75rem 1rem',
22
+ borderRadius: 6,
23
+ border: '1px solid #f5c2c7',
24
+ background: '#f8d7da',
25
+ color: '#842029',
26
+ fontSize: 14,
27
+ }}
28
+ >
29
+ <strong>{title}</strong>
30
+ {message && <div style={{ marginTop: 4 }}>{message}</div>}
31
+ </div>
32
+ );
33
+
34
+ // ── MF Runtime singleton ────────────────────────────────────────────────────
35
+
36
+ type MFInstance = ReturnType<typeof createInstance>;
37
+
38
+ let mfInstance: MFInstance | null = null;
39
+ let registryInstance: ComponentRegistry | null = null;
40
+
41
+ function ensureMFInstance(options?: CreateMFRegistryOptions): MFInstance {
42
+ if (mfInstance) return mfInstance;
43
+
44
+ const baseShared = {
45
+ react: {
46
+ version: React.version,
47
+ scope: 'default',
48
+ lib: () => React,
49
+ shareConfig: {
50
+ singleton: true,
51
+ requiredVersion: '^18.0.0',
52
+ },
53
+ },
54
+ 'react-dom': {
55
+ version: React.version,
56
+ scope: 'default',
57
+ lib: () => ReactDOM,
58
+ shareConfig: {
59
+ singleton: true,
60
+ requiredVersion: '^18.0.0',
61
+ },
62
+ },
63
+ '@a2ui/react': {
64
+ version: '0.8.0',
65
+ scope: 'default',
66
+ lib: () => A2UI,
67
+ shareConfig: {
68
+ singleton: true,
69
+ requiredVersion: '^0.8.0',
70
+ },
71
+ },
72
+ } as const;
73
+
74
+ const shared = {
75
+ ...baseShared,
76
+ ...(options?.shared ?? {}),
77
+ } as any;
78
+
79
+ mfInstance = createInstance({
80
+ name: 'aui_mcp_client',
81
+ remotes: [],
82
+ shared,
83
+ });
84
+
85
+ return mfInstance;
86
+ }
87
+
88
+ /**
89
+ * Create (or get) a `ComponentRegistry` instance and initialize the default catalog.
90
+ *
91
+ * - Share `react` / `react-dom` / `@a2ui/react` as singletons
92
+ * - Allow extra shared deps via `options.shared`
93
+ * - Optionally expose `window.__AUI_REGISTRY__` via `options.exposeGlobal`
94
+ */
95
+ export function createMFRegistry(options?: CreateMFRegistryOptions): ComponentRegistry {
96
+ ensureMFInstance(options);
97
+
98
+ if (!registryInstance) {
99
+ registryInstance = ComponentRegistry.getInstance();
100
+ registerDefaultCatalog(registryInstance);
101
+ logger.info('[aui-mcp-server/react] ComponentRegistry initialized with default catalog');
102
+ }
103
+
104
+ if (typeof window !== 'undefined' && options?.exposeGlobal) {
105
+ (window as any).__AUI_REGISTRY__ = registryInstance;
106
+ }
107
+
108
+ return registryInstance;
109
+ }
110
+
111
+ /**
112
+ * Register a single MF component (lazy-loaded with a safe ErrorPlaceholder fallback).
113
+ */
114
+ export function registerMFComponent(
115
+ registry: ComponentRegistry,
116
+ componentType: string,
117
+ xLoader: XLoaderConfig,
118
+ ): void {
119
+ const mf = ensureMFInstance();
120
+ const { url, scope, module: modulePath } = xLoader;
121
+
122
+ const exposeName = modulePath.replace(/^\.\//, '');
123
+ const remoteId = `${scope}/${exposeName}`;
124
+
125
+ const LazyComponent = lazy(async () => {
126
+ logger.debug(`[AUI-X] MF lazy load triggered: ${componentType} (${remoteId})`);
127
+ try {
128
+ mf.registerRemotes([{ name: scope, entry: url }], { force: false });
129
+
130
+ const mod = await mf.loadRemote<{
131
+ default: React.ComponentType<Record<string, unknown>>;
132
+ }>(remoteId);
133
+
134
+ if (!mod) {
135
+ throw new Error(`[AUI-X] loadRemote returned null for "${remoteId}"`);
136
+ }
137
+
138
+ const Component =
139
+ mod.default ?? (mod as unknown as React.ComponentType<Record<string, unknown>>);
140
+ logger.info(`[AUI-X] ✅ MF component loaded OK: ${componentType}`);
141
+ return { default: Component };
142
+ } catch (error) {
143
+ logger.error(
144
+ `[AUI-X] ❌ Failed to load MF component "${componentType}" from "${remoteId}":`,
145
+ error,
146
+ );
147
+
148
+ const Fallback: React.FC = () => (
149
+ <ErrorPlaceholder
150
+ title="Failed to load MF component"
151
+ message={`Unable to load remote component: ${componentType}`}
152
+ />
153
+ );
154
+
155
+ return { default: Fallback };
156
+ }
157
+ });
158
+
159
+ const registration: ComponentRegistration = {
160
+ component: LazyComponent as never,
161
+ };
162
+
163
+ registry.register(componentType, registration as never);
164
+ logger.info(
165
+ `[AUI-X] MF component registered (runtime): ${componentType} ← ${url} → ${remoteId}`,
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Register all `x-loader` components from a Catalog into the given registry.
171
+ */
172
+ export function syncRegistryFromCatalog(
173
+ registry: ComponentRegistry,
174
+ catalog: Record<string, unknown>,
175
+ ): void {
176
+ Object.entries(catalog).forEach(([type, schema]) => {
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ const anySchema = schema as any;
179
+ const xLoader = (anySchema.x_loader || anySchema['x-loader']) as XLoaderConfig | undefined;
180
+ if (!xLoader || xLoader.type !== 'module-federation') return;
181
+
182
+ registerMFComponent(registry, type, xLoader);
183
+ });
184
+ }