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,183 @@
1
+ import type {
2
+ AuiXCatalogEntry,
3
+ AuiXLoaderConfig,
4
+ AuiXToolDefinition,
5
+ PropDefinition,
6
+ RemoteContext,
7
+ } from '../types';
8
+ import { join, dirname } from 'node:path';
9
+ import { parseToolLogic } from './tool_parser';
10
+
11
+ function propsToSchemaProperties(props: PropDefinition[]): AuiXCatalogEntry['properties'] {
12
+ const result: AuiXCatalogEntry['properties'] = {};
13
+
14
+ for (const prop of props) {
15
+ result[prop.name] = {
16
+ type: 'object',
17
+ ...(prop.description ? { description: prop.description } : {}),
18
+ properties: {
19
+ literalString: { type: 'string' },
20
+ path: { type: 'string' },
21
+ },
22
+ };
23
+ }
24
+
25
+ return result;
26
+ }
27
+
28
+ function getRequiredProps(props: PropDefinition[]): string[] {
29
+ return props.filter((p) => !p.optional).map((p) => p.name);
30
+ }
31
+
32
+ function buildXLoader(ctx: RemoteContext): AuiXLoaderConfig {
33
+ return {
34
+ type: 'module-federation',
35
+ url: ctx.remoteEntryUrl,
36
+ scope: ctx.stats.id,
37
+ module: ctx.expose.path,
38
+ };
39
+ }
40
+
41
+ function buildFallbackExample(ctx: RemoteContext): unknown {
42
+ const surfaceId = `${ctx.componentName.toLowerCase()}-surface-1`;
43
+ const componentId = `${ctx.componentName.toLowerCase()}-card`;
44
+
45
+ const required = getRequiredProps(ctx.props);
46
+ const propsPayload: Record<string, unknown> = {};
47
+ for (const name of required) {
48
+ propsPayload[name] = { literalString: `<!-- ${name} -->` };
49
+ }
50
+
51
+ return [
52
+ {
53
+ beginRendering: {
54
+ surfaceId,
55
+ root: 'root-column',
56
+ },
57
+ },
58
+ {
59
+ surfaceUpdate: {
60
+ surfaceId,
61
+ components: [
62
+ {
63
+ id: 'root-column',
64
+ component: {
65
+ Column: {
66
+ children: { explicitList: [componentId] },
67
+ },
68
+ },
69
+ },
70
+ {
71
+ id: componentId,
72
+ component: {
73
+ [ctx.componentName]: propsPayload,
74
+ },
75
+ },
76
+ ],
77
+ },
78
+ },
79
+ ];
80
+ }
81
+
82
+ export function generateCatalogEntry(ctx: RemoteContext): AuiXCatalogEntry {
83
+ const schemaProps = propsToSchemaProperties(ctx.props);
84
+ const required = getRequiredProps(ctx.props);
85
+
86
+ // ─── A2UI v0.9 Actions (Custom Props) ──────────────────────────────────────
87
+ // Convert `@action` tags into Action properties in the schema.
88
+ if (ctx.jsdoc.actions && ctx.jsdoc.actions.length > 0) {
89
+ for (const actionDef of ctx.jsdoc.actions) {
90
+ const propName = `${actionDef.name}Action`;
91
+ const desc = actionDef.description
92
+ ? `${actionDef.description}${actionDef.replyWith ? ` (Expected reply: ${actionDef.replyWith})` : ''}`
93
+ : `Trigger server event: ${actionDef.name}`;
94
+
95
+ schemaProps[propName] = {
96
+ type: 'object',
97
+ description: desc,
98
+ properties: {
99
+ name: { type: 'string' },
100
+ context: {
101
+ type: 'array',
102
+ items: {
103
+ type: 'object',
104
+ properties: {
105
+ key: { type: 'string' },
106
+ value: { type: 'object' },
107
+ },
108
+ required: ['key', 'value'],
109
+ },
110
+ },
111
+ },
112
+ required: ['name'],
113
+ };
114
+ }
115
+ }
116
+
117
+ const entry: AuiXCatalogEntry = {
118
+ type: 'object',
119
+ description: ctx.jsdoc.description ?? `AUI-X MF Remote component: ${ctx.componentName}.`,
120
+ properties: schemaProps,
121
+ ...(required.length > 0 ? { required } : {}),
122
+ 'x-loader': buildXLoader(ctx),
123
+ 'x-example': ctx.jsdoc.example ?? buildFallbackExample(ctx),
124
+ ...(ctx.jsdoc.authRequired !== undefined
125
+ ? { 'x-auth-required': ctx.jsdoc.authRequired }
126
+ : {}),
127
+ ...(ctx.jsdoc.fallback ? { 'x-fallback': ctx.jsdoc.fallback } : {}),
128
+ };
129
+
130
+ // ─── x-tools wiring ───────────────────────────────────────────────────────
131
+ // 1) Prefer explicit `@tool <toolName> <logicFile>` tags.
132
+ const xTools: AuiXToolDefinition[] = [];
133
+
134
+ if (ctx.jsdoc.tools && ctx.jsdoc.tools.length > 0) {
135
+ for (const tool of ctx.jsdoc.tools) {
136
+ const sourceFile = ctx.expose.file;
137
+ const baseDir = join(process.cwd(), dirname(sourceFile));
138
+
139
+ const parsed = parseToolLogic(tool.name, tool.logicFile, baseDir);
140
+
141
+ if (parsed && ctx.loaderExpose?.path) {
142
+ xTools.push({
143
+ name: parsed.name,
144
+ description: parsed.description,
145
+ loader: ctx.loaderExpose.path,
146
+ parameters: parsed.parameters,
147
+ logicFilePath: parsed.logicFilePath,
148
+ });
149
+ }
150
+ }
151
+ }
152
+
153
+ // 2) Auto-detect MF 2.0 data loader as a tool.
154
+ if (ctx.loaderExpose) {
155
+ const loaderFile = ctx.loaderExpose.file;
156
+ const baseDir = process.cwd();
157
+
158
+ // Auto-generated tool naming convention: get_<ComponentName>_data
159
+ const defaultToolName = `get_${ctx.componentName.toLowerCase()}_data`;
160
+
161
+ const parsed = parseToolLogic(defaultToolName, loaderFile, baseDir);
162
+
163
+ if (parsed) {
164
+ // If the loader file defines an explicit `@tool`, `parseToolLogic` returns that tool name.
165
+ // Only add it when not duplicated.
166
+ if (!xTools.find(t => t.name === parsed.name || t.logicFilePath === parsed.logicFilePath)) {
167
+ xTools.push({
168
+ name: parsed.name,
169
+ description: parsed.description,
170
+ loader: ctx.loaderExpose.path,
171
+ parameters: parsed.parameters,
172
+ logicFilePath: parsed.logicFilePath,
173
+ });
174
+ }
175
+ }
176
+ }
177
+
178
+ if (xTools.length > 0) {
179
+ entry['x-tools'] = xTools;
180
+ }
181
+
182
+ return entry;
183
+ }
@@ -0,0 +1,173 @@
1
+ import type { AuiJsDocTags, PropDefinition } from '../types';
2
+
3
+ /**
4
+ * Parse the first JSDoc block that contains `@aui-component`.
5
+ */
6
+ export function parseJsDoc(dtsContent: string): AuiJsDocTags {
7
+ const jsdocBlockRegex = /\/\*\*([\s\S]*?)\*\//g;
8
+ let targetBlock: string | null = null;
9
+
10
+ for (const match of dtsContent.matchAll(jsdocBlockRegex)) {
11
+ const block = match[1];
12
+ if (block?.includes('@aui-component')) {
13
+ targetBlock = block;
14
+ break;
15
+ }
16
+ }
17
+
18
+ if (!targetBlock) {
19
+ return { isAuiComponent: false };
20
+ }
21
+
22
+ const result: AuiJsDocTags = { isAuiComponent: true };
23
+
24
+ const lines = targetBlock
25
+ .split('\n')
26
+ .map((l) => l.replace(/^\s*\*\s?/, '').trimEnd());
27
+
28
+ const descMatch = lines.find((l) => l.startsWith('@description'));
29
+ if (descMatch) {
30
+ result.description = descMatch.replace('@description', '').trim();
31
+ }
32
+
33
+ const authMatch = lines.find((l) => l.startsWith('@auth-required'));
34
+ if (authMatch) {
35
+ const val = authMatch.replace('@auth-required', '').trim().toLowerCase();
36
+ result.authRequired = val === 'true';
37
+ }
38
+
39
+ const fallbackMatch = lines.find((l) => l.startsWith('@fallback'));
40
+ if (fallbackMatch) {
41
+ result.fallback = fallbackMatch.replace('@fallback', '').trim();
42
+ }
43
+
44
+ const exampleStartIdx = lines.findIndex((l) => l.startsWith('@example'));
45
+ if (exampleStartIdx !== -1) {
46
+ const exampleLine = lines[exampleStartIdx];
47
+ if (exampleLine) {
48
+ const exampleFirstLine = exampleLine.replace('@example', '').trim();
49
+
50
+ const continuationLines: string[] = [];
51
+ for (let i = exampleStartIdx + 1; i < lines.length; i++) {
52
+ const l = lines[i];
53
+ if (!l) break;
54
+ if (/^@\w/.test(l)) break;
55
+ if (l && !/^[\s[\]{},"'0-9\-+tfn]/.test(l)) break;
56
+ continuationLines.push(l);
57
+ }
58
+
59
+ const rawJson = [exampleFirstLine, ...continuationLines].join('\n').trim();
60
+ if (rawJson) {
61
+ try {
62
+ result.example = JSON.parse(rawJson);
63
+ } catch {
64
+ // keep silent and fallback later
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ // ── @tool parsing ─────────────────────────────────────────────────────────
71
+ // Supported syntax:
72
+ // @tool <toolName> <logicFile>
73
+ // Example:
74
+ // @tool get_concert_tours ./getConcertTours
75
+ const toolLines = lines.filter((l) => l.startsWith('@tool '));
76
+ if (toolLines.length > 0) {
77
+ result.tools = toolLines.map((l) => {
78
+ const parts = l.replace('@tool', '').trim().split(/\s+/);
79
+ return {
80
+ name: parts[0] ?? '',
81
+ logicFile: parts[1] ?? '',
82
+ };
83
+ });
84
+ }
85
+
86
+ // ── @action parsing ───────────────────────────────────────────────────────
87
+ // Supported syntax:
88
+ // @action name="wish_more_shows" replyWith="WishShowForm" contextKeys="artist,region" description="Wish for more tour shows"
89
+ const actionLines = lines.filter((l) => l.startsWith('@action '));
90
+ if (actionLines.length > 0) {
91
+ result.actions = actionLines.map((l) => {
92
+ const raw = l.replace('@action', '').trim();
93
+ const action: any = {};
94
+ const pairs = raw.match(/(\w+)="([^"]*)"/g) || [];
95
+ for (const p of pairs) {
96
+ const [k, v] = p.split('="');
97
+ const key = k?.trim();
98
+ const val = v?.replace(/"$/, '');
99
+ if (key === 'name') action.name = val;
100
+ if (key === 'description') action.description = val;
101
+ if (key === 'replyWith') action.replyWith = val;
102
+ if (key === 'contextKeys') action.contextKeys = val?.split(',').map((s: string) => s.trim());
103
+ }
104
+ return action;
105
+ });
106
+ }
107
+
108
+ return result;
109
+ }
110
+
111
+ export function needsJsDocFill(dtsContent: string): boolean {
112
+ return !dtsContent.includes('@aui-component');
113
+ }
114
+
115
+ /**
116
+ * Parse props interface in compiled d.ts.
117
+ */
118
+ export function parsePropsInterface(dtsContent: string, componentName: string): PropDefinition[] {
119
+ const patterns = [
120
+ new RegExp(`interface\\s+${componentName}Properties\\s*\\{([\\s\\S]*?)\\}`, 'm'),
121
+ new RegExp(`interface\\s+${componentName}Props\\s*\\{([\\s\\S]*?)\\}`, 'm'),
122
+ ];
123
+
124
+ let interfaceBody: string | null = null;
125
+ for (const pattern of patterns) {
126
+ const m = dtsContent.match(pattern);
127
+ const body = m?.[1];
128
+ if (body) {
129
+ interfaceBody = body;
130
+ break;
131
+ }
132
+ }
133
+
134
+ if (!interfaceBody) {
135
+ return [];
136
+ }
137
+
138
+ // Handle nesting: node { properties { ... } }
139
+ // In the AUI spec, actual props are defined under `node.properties`.
140
+ const nodeMatch = interfaceBody.match(/node\s*:\s*\{\s*properties\s*:\s*\{([\s\S]*?)\}\s*\}/m);
141
+ if (nodeMatch) {
142
+ interfaceBody = nodeMatch[1]!;
143
+ }
144
+
145
+ const props: PropDefinition[] = [];
146
+ // A more tolerant regex for matching nested property definitions in the AUI shape.
147
+ const fieldRegex = /^\s*(\w+)(\??):\s*([\s\S]+?)(?:;|$)/;
148
+
149
+ const reserved = ['node', 'properties', 'literalString', 'path'];
150
+
151
+ for (const line of interfaceBody.split('\n')) {
152
+ const m = line.match(fieldRegex);
153
+ if (!m) continue;
154
+
155
+ const name = m[1];
156
+ if (!name || (nodeMatch && reserved.includes(name))) continue;
157
+
158
+ const questionMark = m[2];
159
+ let rawType = m[3]?.trim() ?? '';
160
+
161
+ // If the type is an inline object definition, simplify it to 'object'.
162
+ if (rawType.startsWith('{')) {
163
+ rawType = 'object';
164
+ }
165
+
166
+ const optional = questionMark === '?';
167
+ const type = rawType;
168
+
169
+ props.push({ name, type, optional });
170
+ }
171
+
172
+ return props;
173
+ }
@@ -0,0 +1,145 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, isAbsolute } from 'node:path';
3
+
4
+ export interface ParsedToolLogic {
5
+ name: string;
6
+ description: string;
7
+ parameters: Record<string, unknown>;
8
+ /** Absolute path to the local logic file (if resolved). */
9
+ logicFilePath: string;
10
+ }
11
+
12
+ /**
13
+ * A simple regex-based tool logic parser for AUI-X.
14
+ *
15
+ * Responsibilities:
16
+ * - Resolve a TS/JS file from a logicFilePath hint
17
+ * - Parse JSDoc description
18
+ * - Infer a minimal JSON-schema-like parameters shape from the
19
+ * loader function's argument type
20
+ *
21
+ * NOTE: This function no longer infers or records any HTTP endpoint.
22
+ * Endpoint routing is now owned by the MCP server, which executes
23
+ * MF loaders directly via the Module Federation runtime.
24
+ */
25
+ export function parseToolLogic(
26
+ toolName: string,
27
+ logicFilePath: string,
28
+ baseDir: string
29
+ ): ParsedToolLogic | null {
30
+ // 1. Resolve file path
31
+ let targetPath = '';
32
+ const extensions = ['.ts', '.tsx', '.js', '.jsx'];
33
+
34
+ // Try absolute or relative
35
+ const absPath = isAbsolute(logicFilePath) ? logicFilePath : join(baseDir, logicFilePath);
36
+ for (const ext of ['', ...extensions]) {
37
+ if (existsSync(absPath + ext)) {
38
+ targetPath = absPath + ext;
39
+ break;
40
+ }
41
+ }
42
+
43
+ if (!targetPath) {
44
+ console.error(`[AUI-X] Tool logic file not found: ${logicFilePath} (base: ${baseDir})`);
45
+ return null;
46
+ }
47
+
48
+ const content = readFileSync(targetPath, 'utf-8');
49
+
50
+ // 2. Extract description (JSDoc before the function)
51
+ let description = `Automatically generated tool for ${toolName}.`;
52
+
53
+ let jsdocContent = '';
54
+ let paramsContent = '';
55
+ let finalToolName = toolName;
56
+
57
+ // Search for ANY block that contains @tool
58
+ const jsdocRegex = /\/\*\*([\s\S]*?)\*\/[\s\S]*?(?:export\s+)?(?:async\s+)?function\s+(\w+)/g;
59
+ let match;
60
+
61
+ while ((match = jsdocRegex.exec(content)) !== null) {
62
+ const block = match[1]!;
63
+ const funcName = match[2]!;
64
+
65
+ // Pattern A: Manually specified @tool
66
+ const toolMatch = block.match(/@tool\s+(\w+)/);
67
+ if (toolMatch) {
68
+ // If we are looking for a specific tool name, check if it matches
69
+ // Or if we are looking for the 'loader' default, take the first @tool we find in the loader file
70
+ if (toolMatch[1] === toolName || toolName.includes('artistevents') || toolName.endsWith('_data')) {
71
+ jsdocContent = block;
72
+ finalToolName = toolMatch[1]!;
73
+
74
+ const pRegex = new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${funcName}\\s*\\(\\s*(?:args|\\{[\\s\\S]*?\\}|)\\s*:\\s*\\{([\\s\\S]*?)\\}\\s*\\)`, 'm');
75
+ const pMatch = content.match(pRegex);
76
+ if (pMatch) {
77
+ paramsContent = pMatch[1] || '';
78
+ }
79
+ break;
80
+ }
81
+ }
82
+
83
+ // Pattern B: Default MF 2.0 loader (if no @tool tag found yet)
84
+ if (funcName === 'loader' && !jsdocContent) {
85
+ jsdocContent = block;
86
+ const pRegex = /(?:export\s+)?(?:async\s+)?function\s+loader\s*\(\s*(?:args|\{[\s\S]*?\}|)\s*:\s*\{([\s\S]*?)\}\s*\)/m;
87
+ const pMatch = content.match(pRegex);
88
+ if (pMatch) {
89
+ paramsContent = pMatch[1] || '';
90
+ }
91
+ }
92
+ }
93
+
94
+ const properties: Record<string, any> = {};
95
+ const required: string[] = [];
96
+
97
+ if (jsdocContent) {
98
+ // Check if there is an @description override in JSDoc
99
+ const descriptionOverride = jsdocContent.match(/@description\s+([^\n@]+)/);
100
+ if (descriptionOverride?.[1]) {
101
+ description = descriptionOverride[1].trim();
102
+ } else {
103
+ description = jsdocContent
104
+ .split('\n')
105
+ .map(l => l.replace(/^\s*\*\s?/, '').trim())
106
+ .filter(l => l && !l.startsWith('@') && !l.startsWith('/') && !l.endsWith('/'))
107
+ .join('\n') // keep line breaks first
108
+ .trim();
109
+
110
+ // If the extracted description still contains @tool-like remnants, clean it again
111
+ // (helps with some non-standard JSDoc indentation).
112
+ description = description.split('\n').filter(l => !l.trim().startsWith('@')).join(' ').trim();
113
+ }
114
+
115
+ // Parse parameters
116
+ if (paramsContent) {
117
+ const fields = paramsContent.split(',').map(f => f.trim()).filter(Boolean);
118
+ for (const field of fields) {
119
+ const fieldMatch = field.match(/^(\w+)(\??):\s*(\w+)/);
120
+ if (fieldMatch) {
121
+ const name = fieldMatch[1]!;
122
+ const optional = fieldMatch[2] === '?';
123
+ const rawType = fieldMatch[3]!;
124
+ let type = 'string';
125
+ if (rawType === 'number') type = 'number';
126
+ if (rawType === 'boolean') type = 'boolean';
127
+ properties[name] = { type };
128
+ if (!optional) required.push(name);
129
+ }
130
+ }
131
+ }
132
+
133
+ }
134
+
135
+ return {
136
+ name: finalToolName,
137
+ description,
138
+ logicFilePath: targetPath,
139
+ parameters: {
140
+ type: 'object',
141
+ properties,
142
+ required: required.length > 0 ? required : undefined,
143
+ } as any,
144
+ };
145
+ }