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.
- package/README.md +122 -0
- package/dist/cli.cjs +1088 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1076 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/index.cjs +619 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +194 -0
- package/dist/client/index.d.ts +194 -0
- package/dist/client/index.js +584 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.cjs +1053 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +163 -0
- package/dist/index.d.ts +163 -0
- package/dist/index.js +1036 -0
- package/dist/index.js.map +1 -0
- package/dist/rsbuild.cjs +1049 -0
- package/dist/rsbuild.cjs.map +1 -0
- package/dist/rsbuild.d.cts +12 -0
- package/dist/rsbuild.d.ts +12 -0
- package/dist/rsbuild.js +1038 -0
- package/dist/rsbuild.js.map +1 -0
- package/dist/rspack.cjs +1016 -0
- package/dist/rspack.cjs.map +1 -0
- package/dist/rspack.d.cts +40 -0
- package/dist/rspack.d.ts +40 -0
- package/dist/rspack.js +1005 -0
- package/dist/rspack.js.map +1 -0
- package/dist/server.cjs +304 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +16 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +297 -0
- package/dist/server.js.map +1 -0
- package/package.json +72 -0
- package/src/catalog/build.ts +89 -0
- package/src/catalog/entry.ts +183 -0
- package/src/catalog/parser.ts +173 -0
- package/src/catalog/tool_parser.ts +145 -0
- package/src/cli.ts +318 -0
- package/src/client/handshake.ts +166 -0
- package/src/client/index.ts +6 -0
- package/src/client/registry.tsx +184 -0
- package/src/client/types.ts +136 -0
- package/src/client/useA2UIStream.ts +378 -0
- package/src/client/useLogger.ts +147 -0
- package/src/generator.ts +100 -0
- package/src/index.ts +17 -0
- package/src/mcp-app-poc.html +69 -0
- package/src/poc.ts +88 -0
- package/src/rsbuild.ts +46 -0
- package/src/rspack.ts +282 -0
- package/src/server.ts +391 -0
- package/src/templates.ts +51 -0
- package/src/types.ts +195 -0
- package/src/utils.ts +29 -0
- package/test.js +16 -0
- package/tsconfig.json +19 -0
- 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,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
|
+
}
|