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
@@ -0,0 +1,100 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import esbuild from 'esbuild';
5
+
6
+ import type {
7
+ AuiMcpManifest,
8
+ AuiXCatalog,
9
+ GenerateAuiMcpAssetsOptions,
10
+ GenerateAuiMcpAssetsResult,
11
+ } from './types';
12
+ import { fileExists, readJsonFile, toToolName } from './utils';
13
+ import { buildServerSource } from './templates';
14
+
15
+ function assertCatalogShape(catalog: unknown): asserts catalog is AuiXCatalog {
16
+ if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) {
17
+ throw new Error('Invalid aui-x-catalog.json: expected an object');
18
+ }
19
+ }
20
+
21
+ async function loadCatalog(options: GenerateAuiMcpAssetsOptions): Promise<AuiXCatalog> {
22
+ const catalogPath = options.catalogPath ?? 'aui-x-catalog.json';
23
+ if (!(await fileExists(catalogPath))) {
24
+ throw new Error(`catalog not found: ${catalogPath}`);
25
+ }
26
+ const catalog = await readJsonFile<unknown>(catalogPath);
27
+ assertCatalogShape(catalog);
28
+ return catalog;
29
+ }
30
+
31
+ export function buildAuiMcpManifest(catalog: AuiXCatalog): AuiMcpManifest {
32
+ const tools = Object.entries(catalog).map(([componentName, entry]) => {
33
+ return {
34
+ name: toToolName(componentName),
35
+ componentName,
36
+ description: entry.description,
37
+ inputSchema: { type: 'object', properties: {}, required: [] },
38
+ entry,
39
+ };
40
+ });
41
+
42
+ return {
43
+ schemaVersion: 1,
44
+ tools,
45
+ };
46
+ }
47
+
48
+ export async function bundleAuiMcpServer(): Promise<string> {
49
+ const serverSource = buildServerSource();
50
+
51
+ const result = await esbuild.build({
52
+ stdin: {
53
+ contents: serverSource,
54
+ // Resolve from package root so pnpm symlinked deps can be bundled.
55
+ resolveDir: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'),
56
+ sourcefile: 'aui-mcp-server.generated.ts',
57
+ loader: 'ts',
58
+ },
59
+ bundle: true,
60
+ platform: 'node',
61
+ format: 'esm',
62
+ target: 'node18',
63
+ write: false,
64
+ outfile: 'server.mjs',
65
+ sourcemap: false,
66
+ logLevel: 'silent',
67
+ });
68
+
69
+ const outputFile = result.outputFiles?.[0];
70
+ if (!outputFile) {
71
+ throw new Error('Failed to bundle MCP server');
72
+ }
73
+
74
+ return outputFile.text;
75
+ }
76
+
77
+ export async function generateAuiMcpAssets(
78
+ options: GenerateAuiMcpAssetsOptions = {}
79
+ ): Promise<GenerateAuiMcpAssetsResult> {
80
+ const outDir = options.outDir ?? 'dist';
81
+ const mcpDirname = options.mcpDirname ?? 'mcp';
82
+ const mcpDir = path.join(outDir, mcpDirname);
83
+
84
+ const catalog = await loadCatalog(options);
85
+ const manifest = buildAuiMcpManifest(catalog);
86
+
87
+ await fs.mkdir(mcpDir, { recursive: true });
88
+
89
+ const manifestPath = path.join(mcpDir, 'manifest.json');
90
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
91
+
92
+ let serverPath: string | undefined;
93
+ if (options.emitServer !== false) {
94
+ serverPath = path.join(mcpDir, 'server.mjs');
95
+ const serverCode = await bundleAuiMcpServer();
96
+ await fs.writeFile(serverPath, serverCode, 'utf-8');
97
+ }
98
+
99
+ return { manifestPath, serverPath };
100
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type {
2
+ AuiMcpManifest,
3
+ AuiMcpTool,
4
+ AuiXCatalog,
5
+ AuiXCatalogEntry,
6
+ AuiXLoaderConfig,
7
+ GenerateAuiMcpAssetsOptions,
8
+ GenerateAuiMcpAssetsResult,
9
+ MfStats,
10
+ } from './types';
11
+
12
+ export { buildAuiMcpManifest, bundleAuiMcpServer, generateAuiMcpAssets } from './generator';
13
+ export { serveAuiMcpServer } from './server';
14
+ export type { ServeAuiMcpServerOptions, TransportType } from './server';
15
+ export { buildCatalogFromStats } from './catalog/build';
16
+ export { auiMcpRsbuildPlugin } from './rsbuild';
17
+ export { AuiMcpRspackPlugin } from './rspack';
@@ -0,0 +1,69 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AUI-X MCP POC</title>
7
+ <style>
8
+ body { font-family: sans-serif; padding: 20px; background: #f0f0f0; }
9
+ .container { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
10
+ .status { color: #666; font-size: 0.9em; margin-bottom: 10px; }
11
+ .result { border-top: 1px solid #eee; margin-top: 10px; padding-top: 10px; }
12
+ .loader { color: #007bff; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <div class="container">
17
+ <h1>AUI-X MCP POC</h1>
18
+ <div id="status" class="status">Connecting to MCP host...</div>
19
+ <div id="content">
20
+ <p>This is a POC for the Iframe-based UI in AUI-X.</p>
21
+ </div>
22
+ <div id="result" class="result" style="display:none">
23
+ <h3>Tool Result:</h3>
24
+ <pre id="tool-output"></pre>
25
+ </div>
26
+ </div>
27
+
28
+ <script type="module">
29
+ // For POC, we'll use a simple postMessage bridge if @modelcontextprotocol/ext-apps is not available as a CDN resource easily
30
+ // But for verification, we'll try to listen to message events which is what the SDK uses under the hood
31
+
32
+ const statusEl = document.getElementById('status');
33
+ const resultEl = document.getElementById('result');
34
+ const outputEl = document.getElementById('tool-output');
35
+
36
+ window.addEventListener('message', (event) => {
37
+ console.log('Received message:', event.data);
38
+
39
+ // The MCP Apps SDK uses a specific message format for handshake and protocol messages
40
+ if (event.data && event.data.method === 'initialize') {
41
+ statusEl.innerText = 'Connected to MCP host';
42
+ // Send initialize response
43
+ event.source.postMessage({
44
+ jsonrpc: '2.0',
45
+ id: event.data.id,
46
+ result: {
47
+ protocolVersion: '2024-11-05',
48
+ capabilities: {},
49
+ serverInfo: { name: 'aui-x-poc', version: '0.1.0' }
50
+ }
51
+ }, event.origin);
52
+ }
53
+
54
+ // Handle tool results
55
+ if (event.data && event.data.method === 'notifications/toolResult') {
56
+ const result = event.data.params;
57
+ statusEl.innerText = 'Tool result received';
58
+ resultEl.style.display = 'block';
59
+ outputEl.innerText = JSON.stringify(result, null, 2);
60
+ }
61
+ });
62
+
63
+ // Signal that we are ready
64
+ if (window.parent !== window) {
65
+ window.parent.postMessage({ type: 'mcp-app-ready' }, '*');
66
+ }
67
+ </script>
68
+ </body>
69
+ </html>
package/src/poc.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { z } from 'zod';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ async function main() {
12
+ const server = new McpServer({
13
+ name: 'aui-x-poc-server',
14
+ version: '1.0.0',
15
+ });
16
+
17
+ const POC_RESOURCE_URI = 'ui://aui-x/poc';
18
+
19
+ // 1. Register the POC Tool
20
+ registerAppTool(
21
+ server,
22
+ 'aui_x_poc_tool',
23
+ {
24
+ title: 'AUI-X POC Tool',
25
+ description: 'A simple tool to demonstrate AUI-X Iframe integration',
26
+ inputSchema: z.object({
27
+ message: z.string().optional().describe('A message to display in the POC UI'),
28
+ }),
29
+ _meta: {
30
+ ui: {
31
+ resourceUri: POC_RESOURCE_URI,
32
+ },
33
+ },
34
+ },
35
+ async (args) => {
36
+ console.error('[POC] Tool called with:', args);
37
+ const payload = {
38
+ tool: 'aui_x_poc_tool',
39
+ args,
40
+ resource: {
41
+ uri: POC_RESOURCE_URI,
42
+ mimeType: 'text/html;profile=mcp-app'
43
+ }
44
+ };
45
+ return {
46
+ content: [{ type: 'text', text: JSON.stringify(payload) }],
47
+ structuredContent: payload,
48
+ };
49
+ }
50
+ );
51
+
52
+ // 2. Register the POC Resource
53
+ registerAppResource(
54
+ server,
55
+ 'aui-x-poc-resource',
56
+ POC_RESOURCE_URI,
57
+ { mimeType: RESOURCE_MIME_TYPE },
58
+ async () => {
59
+ console.error('[POC] Resource requested:', POC_RESOURCE_URI);
60
+ const htmlPath = path.join(__dirname, 'mcp-app-poc.html');
61
+ const html = await fs.readFile(htmlPath, 'utf-8');
62
+ return {
63
+ contents: [
64
+ {
65
+ uri: POC_RESOURCE_URI,
66
+ mimeType: RESOURCE_MIME_TYPE,
67
+ text: html,
68
+ _meta: {
69
+ ui: {
70
+ prefersBorder: true,
71
+ },
72
+ },
73
+ },
74
+ ],
75
+ };
76
+ }
77
+ );
78
+
79
+ console.error('[POC] Server starting...');
80
+ const transport = new StdioServerTransport();
81
+ await server.connect(transport);
82
+ console.error('[POC] Server connected via stdio');
83
+ }
84
+
85
+ main().catch((err) => {
86
+ console.error('[POC] Fatal error:', err);
87
+ process.exit(1);
88
+ });
package/src/rsbuild.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { AuiMcpRspackPluginOptions } from './rspack';
2
+ import { AuiMcpRspackPlugin } from './rspack';
3
+ import { createRequire } from 'node:module';
4
+
5
+ /**
6
+ * Rsbuild plugin wrapper.
7
+ *
8
+ * It injects `AuiMcpRspackPlugin` into the underlying Rspack config.
9
+ */
10
+ export function auiMcpRsbuildPlugin(options: AuiMcpRspackPluginOptions = {}): any {
11
+ return {
12
+ // Rslib `format: 'mf'` expects this plugin to exist in config.plugins.
13
+ name: 'rsbuild:module-federation-enhanced',
14
+ setup(api: any) {
15
+ const mfOptions = options.mfOptions;
16
+ if (!mfOptions || typeof mfOptions !== 'object') {
17
+ throw new Error('[aui-mcp-server] Missing required option: mfOptions');
18
+ }
19
+
20
+ // Ensure rslib/rsbuild mf pipeline is enabled without requiring users to add MF plugin themselves.
21
+ // We load it lazily so non-rsbuild consumers don't need this dependency.
22
+ try {
23
+ const require = createRequire(import.meta.url);
24
+ const { pluginModuleFederation } = require('@module-federation/rsbuild-plugin') as any;
25
+ pluginModuleFederation(mfOptions,{target:'dual'}).setup?.(api);
26
+ } catch (err) {
27
+ throw new Error(
28
+ `[aui-mcp-server] Failed to load @module-federation/rsbuild-plugin. Please install it. ` +
29
+ `${err instanceof Error ? err.message : String(err)}`
30
+ );
31
+ }
32
+
33
+ api.modifyRspackConfig?.((config: any, utils: any) => {
34
+ utils.appendPlugins(
35
+ new AuiMcpRspackPlugin({
36
+ ...options,
37
+ mfOptions,
38
+ // MF plugin is handled by @module-federation/rsbuild-plugin above.
39
+ applyModuleFederationPlugin: false,
40
+ })
41
+ );
42
+ return config;
43
+ });
44
+ },
45
+ };
46
+ }
package/src/rspack.ts ADDED
@@ -0,0 +1,282 @@
1
+ import fs from 'node:fs';
2
+ import { promises as fsp } from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import type { Compiler, RspackPluginInstance } from '@rspack/core';
6
+ import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
7
+
8
+ import type { MfStats } from './types';
9
+ import { buildCatalogFromStats } from './catalog/build';
10
+ import { generateAuiMcpAssets } from './generator';
11
+ import type { TransportType } from './server';
12
+ import { serveAuiMcpServer } from './server';
13
+
14
+ export interface AuiMcpRspackPluginOptions {
15
+ /** Module Federation options (will be passed to MF plugin). */
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ mfOptions?: any;
18
+
19
+ /** If false, will NOT apply ModuleFederationPlugin (useful when MF is managed by a rsbuild plugin). */
20
+ applyModuleFederationPlugin?: boolean;
21
+
22
+ /** dist output directory (absolute or relative to compiler.context). Defaults to compiler output.path */
23
+ outDir?: string;
24
+
25
+ /** Sub dir under dist for MCP assets. Defaults to "mcp" */
26
+ mcpDirname?: string;
27
+
28
+ /** Name of stats asset emitted by MF plugin. Defaults to "mf-stats.json" */
29
+ statsAssetName?: string;
30
+
31
+ /** Name of generated catalog json in dist root. Defaults to "aui-x-catalog.json" */
32
+ catalogAssetName?: string;
33
+
34
+ /** Whether to emit an executable MCP server bundle. Defaults to true */
35
+ emitServer?: boolean;
36
+
37
+ /** Auto start MCP server in watch/dev mode */
38
+ dev?: boolean;
39
+
40
+ /** Transport type used by auto-started server */
41
+ transport?: TransportType;
42
+
43
+ /** SSE port used by auto-started server */
44
+ port?: number;
45
+ }
46
+
47
+ function isRecord(value: unknown): value is Record<string, unknown> {
48
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
49
+ }
50
+
51
+ export class AuiMcpRspackPlugin implements RspackPluginInstance {
52
+ private options: AuiMcpRspackPluginOptions;
53
+ private devServerStarted = false;
54
+
55
+ constructor(options: AuiMcpRspackPluginOptions = {}) {
56
+ this.options = options;
57
+ }
58
+
59
+ apply(compiler: Compiler): void {
60
+ const pluginName = 'aui-mcp-server';
61
+
62
+ const mfOptions = this.options.mfOptions;
63
+ if (!mfOptions || typeof mfOptions !== 'object') {
64
+ throw new Error(`[${pluginName}] Missing required option: mfOptions`);
65
+ }
66
+
67
+ this.injectAfterGenerateTypesHook(compiler, mfOptions);
68
+
69
+ if (this.options.applyModuleFederationPlugin !== false) {
70
+ new ModuleFederationPlugin(mfOptions).apply(compiler);
71
+ }
72
+ }
73
+
74
+ private getOutDir(compiler: any, compilation: any | undefined): string {
75
+ const rawOutDir =
76
+ this.options.outDir ??
77
+ compilation?.outputOptions?.path ??
78
+ compiler?.options?.output?.path ??
79
+ compiler?.outputPath ??
80
+ 'dist';
81
+
82
+ // Prefer process.cwd() because some wrappers (rsbuild/rslib) may set compiler.context
83
+ // to the workspace root while the actual project output (dist) is relative to cwd.
84
+ const context = process.cwd();
85
+ return path.isAbsolute(rawOutDir) ? rawOutDir : path.resolve(context, rawOutDir);
86
+ }
87
+
88
+ private isDev(compiler: any): boolean {
89
+ if (this.options.dev !== undefined) return this.options.dev;
90
+
91
+ const mode = compiler?.options?.mode;
92
+ if (mode === 'development') return true;
93
+ if (compiler?.watchMode) return true;
94
+ return false;
95
+ }
96
+
97
+ private readCompiledTypes(compiler: any, outDir: string, componentName: string): string | undefined {
98
+ // 1) @mf-types/xxx.d.ts (MF 2.0 standard entry)
99
+ const entryPath = path.join(outDir, '@mf-types', `${componentName}.d.ts`);
100
+ let content = this.readFileFromCompilerOrNodeFs(compiler, entryPath);
101
+
102
+ // 2) Fallback: @mf-types/compiled-types/xxx.d.ts
103
+ if (!content) {
104
+ const compiledPath = path.join(outDir, '@mf-types', 'compiled-types', `${componentName}.d.ts`);
105
+ content = this.readFileFromCompilerOrNodeFs(compiler, compiledPath);
106
+ }
107
+
108
+ if (!content) return undefined;
109
+
110
+ // 3) Proxy file: export * from './compiled-types/xxx'
111
+ const match = content.match(/export \* from '\.\/(compiled-types\/[^']+)'/);
112
+ if (!match) return content;
113
+
114
+ const realPath = path.join(outDir, '@mf-types', `${match[1]}.d.ts`);
115
+ return this.readFileFromCompilerOrNodeFs(compiler, realPath);
116
+ }
117
+
118
+ private readFileFromCompilerOrNodeFs(compiler: any, absPath: string): string | undefined {
119
+ // Prefer rspack/webpack outputFileSystem (memfs in dev) if available.
120
+ const ofs = compiler?.outputFileSystem;
121
+ const readFileSync = ofs?.readFileSync;
122
+ if (typeof readFileSync === 'function') {
123
+ try {
124
+ const resolved = readFileSync.call(ofs, absPath);
125
+ return Buffer.isBuffer(resolved) ? resolved.toString('utf-8') : String(resolved);
126
+ } catch {
127
+ // ignore and fallback to node fs
128
+ }
129
+ }
130
+
131
+ try {
132
+ if (fs.existsSync(absPath)) return fs.readFileSync(absPath, 'utf-8');
133
+ } catch {
134
+ // ignore
135
+ }
136
+ return undefined;
137
+ }
138
+
139
+ private async readJsonFromCompilerOrNodeFs<T>(compiler: any, absPath: string): Promise<T> {
140
+ const ofs = compiler?.outputFileSystem;
141
+ const readFile = ofs?.readFile;
142
+ if (typeof readFile === 'function') {
143
+ const data = await new Promise<string>((resolve, reject) => {
144
+ readFile.call(ofs, absPath, (err: any, buf: any) => {
145
+ if (err) return reject(err);
146
+ const text = Buffer.isBuffer(buf) ? buf.toString('utf-8') : String(buf);
147
+ resolve(text);
148
+ });
149
+ });
150
+ return JSON.parse(data) as T;
151
+ }
152
+
153
+ const text = await fsp.readFile(absPath, 'utf-8');
154
+ return JSON.parse(text) as T;
155
+ }
156
+
157
+ private async generateCatalogAndMcpAssetsAfterTypes(compiler: Compiler): Promise<string | undefined> {
158
+ const outDir = this.getOutDir(compiler, undefined);
159
+ const statsAssetName = this.options.statsAssetName ?? 'mf-stats.json';
160
+ const statsPath = path.join(outDir, statsAssetName);
161
+
162
+ let stats: MfStats;
163
+ try {
164
+ stats = await this.readJsonFromCompilerOrNodeFs<MfStats>(compiler, statsPath);
165
+ } catch (err) {
166
+ // eslint-disable-next-line no-console
167
+ console.error(
168
+ `[aui-mcp-server] Failed to read ${statsAssetName} from ${statsPath}:`,
169
+ err instanceof Error ? err.message : err
170
+ );
171
+ return undefined;
172
+ }
173
+
174
+ // MF dts generation may still be flushing files in dev; wait briefly for compiled-types.
175
+ const componentNames = (stats.exposes ?? [])
176
+ .filter((e: any) => e && typeof e === 'object' && typeof e.path === 'string' && !e.path.endsWith('.data'))
177
+ .map((e: any) => String(e.name))
178
+ .filter(Boolean);
179
+
180
+ const maxAttempts = 20;
181
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
182
+ const missing = componentNames.filter((name) => !this.readCompiledTypes(compiler, outDir, name));
183
+ if (missing.length === 0) break;
184
+ if (attempt === maxAttempts) break;
185
+ await new Promise((r) => setTimeout(r, Math.min(200 * attempt, 1000)));
186
+ }
187
+
188
+ const { catalog, issues, missingJsdocComponents } = buildCatalogFromStats({
189
+ stats,
190
+ readCompiledTypes: (componentName) => this.readCompiledTypes(compiler, outDir, componentName),
191
+ });
192
+
193
+ if (issues.length > 0) {
194
+ const head = issues
195
+ .slice(0, 10)
196
+ .map((i) => `${i.componentName ?? 'unknown'}: ${i.message}`)
197
+ .join('; ');
198
+ const tail = issues.length > 10 ? ` ... (+${issues.length - 10})` : '';
199
+ // eslint-disable-next-line no-console
200
+ console.warn(`[aui-mcp-server] catalog issues: ${head}${tail}`);
201
+ }
202
+
203
+ if (missingJsdocComponents.length > 0) {
204
+ const names = missingJsdocComponents.slice(0, 10).join(', ');
205
+ const tail = missingJsdocComponents.length > 10 ? ` ... (+${missingJsdocComponents.length - 10})` : '';
206
+ // eslint-disable-next-line no-console
207
+ console.warn(
208
+ `[aui-mcp-server] missing @aui-component JSDoc: ${names}${tail}. Please run skills/aui-jsdoc-gen to write back to source files and retry.`
209
+ );
210
+ }
211
+
212
+ if (!isRecord(catalog) || Object.keys(catalog).length === 0) return undefined;
213
+
214
+ const catalogAssetName = this.options.catalogAssetName ?? 'aui-x-catalog.json';
215
+ const catalogPath = path.resolve(outDir, catalogAssetName);
216
+ await fsp.mkdir(path.dirname(catalogPath), { recursive: true });
217
+ await fsp.writeFile(catalogPath, JSON.stringify(catalog, null, 2) + '\n', 'utf-8');
218
+
219
+ await generateAuiMcpAssets({
220
+ catalogPath,
221
+ outDir,
222
+ mcpDirname: this.options.mcpDirname ?? 'mcp',
223
+ emitServer: this.options.emitServer !== false,
224
+ });
225
+
226
+ return catalogPath;
227
+ }
228
+
229
+ private injectAfterGenerateTypesHook(compiler: Compiler, mfOptions: any): void {
230
+ const pluginName = 'aui-mcp-server';
231
+ const rawDts = mfOptions.dts ?? {};
232
+ mfOptions.dts = isRecord(rawDts) ? rawDts : {};
233
+ const dts = mfOptions.dts as Record<string, unknown>;
234
+
235
+ // MF Plugin expects the hook at: dts.generateTypes.afterGenerate
236
+ const rawGenerateTypes = (dts as any).generateTypes ?? {};
237
+ (dts as any).generateTypes = isRecord(rawGenerateTypes) ? rawGenerateTypes : {};
238
+ const generateTypes = (dts as any).generateTypes as Record<string, unknown>;
239
+
240
+ // Avoid double injection if plugin applied twice.
241
+ if ((generateTypes as any).__auiMcpInjectedAfterGenerate) return;
242
+ (generateTypes as any).__auiMcpInjectedAfterGenerate = true;
243
+
244
+ const userAfter = (generateTypes as any).afterGenerate as undefined | ((...args: any[]) => any);
245
+
246
+ (generateTypes as any).afterGenerate = async (...args: any[]) => {
247
+ try {
248
+ await Promise.resolve(userAfter?.(...args));
249
+ } catch (err) {
250
+ // eslint-disable-next-line no-console
251
+ console.error(`[${pluginName}] user afterGenerate failed:`, err);
252
+ }
253
+
254
+ let catalogPath: string | undefined;
255
+ try {
256
+ catalogPath = await this.generateCatalogAndMcpAssetsAfterTypes(compiler);
257
+ } catch (err) {
258
+ // eslint-disable-next-line no-console
259
+ console.error(`[${pluginName}] afterGenerate (aui) failed:`, err);
260
+ return;
261
+ }
262
+
263
+ if (!catalogPath) return;
264
+ if (!this.isDev(compiler)) return;
265
+ if (this.devServerStarted) return;
266
+ this.devServerStarted = true;
267
+
268
+ try {
269
+ await serveAuiMcpServer({
270
+ catalogPath,
271
+ watch: true,
272
+ // Dev defaults to SSE so the sample can proxy to /api/tools.
273
+ transport: (this.options.transport ?? 'sse') as TransportType,
274
+ port: this.options.port ?? 8001,
275
+ });
276
+ } catch (err) {
277
+ // eslint-disable-next-line no-console
278
+ console.error(`[${pluginName}] start dev server failed:`, err);
279
+ }
280
+ };
281
+ }
282
+ }