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
|
@@ -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
|
+
}
|