@aria_asi/cli 0.2.0
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/bin/aria.js +168 -0
- package/dist/aria-connector/src/auth-commands.d.ts +28 -0
- package/dist/aria-connector/src/auth-commands.d.ts.map +1 -0
- package/dist/aria-connector/src/auth-commands.js +129 -0
- package/dist/aria-connector/src/auth-commands.js.map +1 -0
- package/dist/aria-connector/src/auth.d.ts +12 -0
- package/dist/aria-connector/src/auth.d.ts.map +1 -0
- package/dist/aria-connector/src/auth.js +31 -0
- package/dist/aria-connector/src/auth.js.map +1 -0
- package/dist/aria-connector/src/auto-mcp.d.ts +23 -0
- package/dist/aria-connector/src/auto-mcp.d.ts.map +1 -0
- package/dist/aria-connector/src/auto-mcp.js +994 -0
- package/dist/aria-connector/src/auto-mcp.js.map +1 -0
- package/dist/aria-connector/src/chat.d.ts +21 -0
- package/dist/aria-connector/src/chat.d.ts.map +1 -0
- package/dist/aria-connector/src/chat.js +332 -0
- package/dist/aria-connector/src/chat.js.map +1 -0
- package/dist/aria-connector/src/codebase-scanner.d.ts +7 -0
- package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -0
- package/dist/aria-connector/src/codebase-scanner.js +6 -0
- package/dist/aria-connector/src/codebase-scanner.js.map +1 -0
- package/dist/aria-connector/src/cognition-log.d.ts +17 -0
- package/dist/aria-connector/src/cognition-log.d.ts.map +1 -0
- package/dist/aria-connector/src/cognition-log.js +19 -0
- package/dist/aria-connector/src/cognition-log.js.map +1 -0
- package/dist/aria-connector/src/config.d.ts +41 -0
- package/dist/aria-connector/src/config.d.ts.map +1 -0
- package/dist/aria-connector/src/config.js +50 -0
- package/dist/aria-connector/src/config.js.map +1 -0
- package/dist/aria-connector/src/connectors/claude-code.d.ts +4 -0
- package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/claude-code.js +204 -0
- package/dist/aria-connector/src/connectors/claude-code.js.map +1 -0
- package/dist/aria-connector/src/connectors/cursor.d.ts +4 -0
- package/dist/aria-connector/src/connectors/cursor.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/cursor.js +63 -0
- package/dist/aria-connector/src/connectors/cursor.js.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts +4 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.js +102 -0
- package/dist/aria-connector/src/connectors/opencode.js.map +1 -0
- package/dist/aria-connector/src/connectors/shell.d.ts +4 -0
- package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/shell.js +58 -0
- package/dist/aria-connector/src/connectors/shell.js.map +1 -0
- package/dist/aria-connector/src/garden-client.d.ts +19 -0
- package/dist/aria-connector/src/garden-client.d.ts.map +1 -0
- package/dist/aria-connector/src/garden-client.js +85 -0
- package/dist/aria-connector/src/garden-client.js.map +1 -0
- package/dist/aria-connector/src/garden-control-plane.d.ts +22 -0
- package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -0
- package/dist/aria-connector/src/garden-control-plane.js +43 -0
- package/dist/aria-connector/src/garden-control-plane.js.map +1 -0
- package/dist/aria-connector/src/harness-client.d.ts +166 -0
- package/dist/aria-connector/src/harness-client.d.ts.map +1 -0
- package/dist/aria-connector/src/harness-client.js +344 -0
- package/dist/aria-connector/src/harness-client.js.map +1 -0
- package/dist/aria-connector/src/hive-client.d.ts +32 -0
- package/dist/aria-connector/src/hive-client.d.ts.map +1 -0
- package/dist/aria-connector/src/hive-client.js +69 -0
- package/dist/aria-connector/src/hive-client.js.map +1 -0
- package/dist/aria-connector/src/index.d.ts +19 -0
- package/dist/aria-connector/src/index.d.ts.map +1 -0
- package/dist/aria-connector/src/index.js +13 -0
- package/dist/aria-connector/src/index.js.map +1 -0
- package/dist/aria-connector/src/install-hooks.d.ts +18 -0
- package/dist/aria-connector/src/install-hooks.d.ts.map +1 -0
- package/dist/aria-connector/src/install-hooks.js +224 -0
- package/dist/aria-connector/src/install-hooks.js.map +1 -0
- package/dist/aria-connector/src/model-context.d.ts +8 -0
- package/dist/aria-connector/src/model-context.d.ts.map +1 -0
- package/dist/aria-connector/src/model-context.js +83 -0
- package/dist/aria-connector/src/model-context.js.map +1 -0
- package/dist/aria-connector/src/persona.d.ts +27 -0
- package/dist/aria-connector/src/persona.d.ts.map +1 -0
- package/dist/aria-connector/src/persona.js +86 -0
- package/dist/aria-connector/src/persona.js.map +1 -0
- package/dist/aria-connector/src/providers/anthropic.d.ts +4 -0
- package/dist/aria-connector/src/providers/anthropic.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/anthropic.js +92 -0
- package/dist/aria-connector/src/providers/anthropic.js.map +1 -0
- package/dist/aria-connector/src/providers/deepseek.d.ts +3 -0
- package/dist/aria-connector/src/providers/deepseek.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/deepseek.js +28 -0
- package/dist/aria-connector/src/providers/deepseek.js.map +1 -0
- package/dist/aria-connector/src/providers/google.d.ts +3 -0
- package/dist/aria-connector/src/providers/google.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/google.js +38 -0
- package/dist/aria-connector/src/providers/google.js.map +1 -0
- package/dist/aria-connector/src/providers/ollama.d.ts +3 -0
- package/dist/aria-connector/src/providers/ollama.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/ollama.js +28 -0
- package/dist/aria-connector/src/providers/ollama.js.map +1 -0
- package/dist/aria-connector/src/providers/openai.d.ts +4 -0
- package/dist/aria-connector/src/providers/openai.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/openai.js +84 -0
- package/dist/aria-connector/src/providers/openai.js.map +1 -0
- package/dist/aria-connector/src/providers/openrouter.d.ts +3 -0
- package/dist/aria-connector/src/providers/openrouter.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/openrouter.js +30 -0
- package/dist/aria-connector/src/providers/openrouter.js.map +1 -0
- package/dist/aria-connector/src/providers/types.d.ts +20 -0
- package/dist/aria-connector/src/providers/types.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/types.js +2 -0
- package/dist/aria-connector/src/providers/types.js.map +1 -0
- package/dist/aria-connector/src/setup-wizard.d.ts +2 -0
- package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -0
- package/dist/aria-connector/src/setup-wizard.js +140 -0
- package/dist/aria-connector/src/setup-wizard.js.map +1 -0
- package/dist/aria-connector/src/types.d.ts +30 -0
- package/dist/aria-connector/src/types.d.ts.map +1 -0
- package/dist/aria-connector/src/types.js +5 -0
- package/dist/aria-connector/src/types.js.map +1 -0
- package/dist/aria-web/src/lib/codebase-scanner.d.ts +127 -0
- package/dist/aria-web/src/lib/codebase-scanner.d.ts.map +1 -0
- package/dist/aria-web/src/lib/codebase-scanner.js +1730 -0
- package/dist/aria-web/src/lib/codebase-scanner.js.map +1 -0
- package/dist/cli-0.2.0.tgz +0 -0
- package/dist/install.sh +13 -0
- package/hooks/aria-harness-via-sdk.mjs +317 -0
- package/hooks/aria-pre-tool-gate.mjs +596 -0
- package/hooks/aria-preprompt-consult.mjs +175 -0
- package/hooks/aria-stop-gate.mjs +222 -0
- package/package.json +47 -0
- package/src/__tests__/auth-commands.test.ts +132 -0
- package/src/auth-commands.ts +175 -0
- package/src/auth.ts +33 -0
- package/src/auto-mcp.ts +1172 -0
- package/src/chat.ts +387 -0
- package/src/codebase-scanner.ts +18 -0
- package/src/cognition-log.ts +30 -0
- package/src/config.ts +94 -0
- package/src/connectors/claude-code.ts +213 -0
- package/src/connectors/cursor.ts +75 -0
- package/src/connectors/opencode.ts +115 -0
- package/src/connectors/shell.ts +72 -0
- package/src/garden-client.ts +98 -0
- package/src/garden-control-plane.ts +108 -0
- package/src/harness-client.ts +454 -0
- package/src/hive-client.ts +104 -0
- package/src/index.ts +26 -0
- package/src/install-hooks.ts +259 -0
- package/src/model-context.ts +88 -0
- package/src/persona.ts +113 -0
- package/src/providers/anthropic.ts +120 -0
- package/src/providers/deepseek.ts +40 -0
- package/src/providers/google.ts +57 -0
- package/src/providers/ollama.ts +43 -0
- package/src/providers/openai.ts +108 -0
- package/src/providers/openrouter.ts +42 -0
- package/src/providers/types.ts +35 -0
- package/src/setup-wizard.ts +177 -0
- package/src/types.ts +32 -0
package/src/auto-mcp.ts
ADDED
|
@@ -0,0 +1,1172 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
2
|
+
import { basename, dirname, extname, join, relative, resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface ToolParameter {
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
required: boolean;
|
|
10
|
+
description: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ToolCandidate {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
source:
|
|
17
|
+
| 'express-route'
|
|
18
|
+
| 'next-api'
|
|
19
|
+
| 'cli-script'
|
|
20
|
+
| 'exported-function'
|
|
21
|
+
| 'fastapi-route';
|
|
22
|
+
filePath: string;
|
|
23
|
+
parameters: ToolParameter[];
|
|
24
|
+
httpMethod?: string;
|
|
25
|
+
routePath?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface McpGenerationResult {
|
|
29
|
+
serverPath: string;
|
|
30
|
+
toolCount: number;
|
|
31
|
+
tools: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RouteMatch {
|
|
35
|
+
method: string;
|
|
36
|
+
path: string;
|
|
37
|
+
filePath: string;
|
|
38
|
+
handlerBody: string;
|
|
39
|
+
jsdoc: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface FunctionMatch {
|
|
43
|
+
name: string;
|
|
44
|
+
filePath: string;
|
|
45
|
+
params: ToolParameter[];
|
|
46
|
+
jsdoc: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function readFileSafe(p: string): string {
|
|
52
|
+
try {
|
|
53
|
+
return readFileSync(p, 'utf-8');
|
|
54
|
+
} catch {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function walkDir(dir: string, extensions: string[]): string[] {
|
|
60
|
+
const results: string[] = [];
|
|
61
|
+
if (!existsSync(dir)) return results;
|
|
62
|
+
try {
|
|
63
|
+
const entries = readdirSync(dir);
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const fullPath = join(dir, entry);
|
|
66
|
+
let stat;
|
|
67
|
+
try {
|
|
68
|
+
stat = statSync(fullPath);
|
|
69
|
+
} catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (stat.isDirectory() && entry !== 'node_modules' && !entry.startsWith('.')) {
|
|
73
|
+
results.push(...walkDir(fullPath, extensions));
|
|
74
|
+
} else if (stat.isFile()) {
|
|
75
|
+
const ext = extname(entry).toLowerCase();
|
|
76
|
+
if (extensions.includes(ext)) {
|
|
77
|
+
results.push(fullPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Permission errors, etc.
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractJSDoc(source: string, beforeLineIdx: number): string {
|
|
88
|
+
const lines = source.split('\n');
|
|
89
|
+
const docLines: string[] = [];
|
|
90
|
+
let i = beforeLineIdx - 1;
|
|
91
|
+
while (i >= 0 && lines[i].trim().startsWith('*')) {
|
|
92
|
+
const cleaned = lines[i].replace(/^\s*\*\s?/, '').trim();
|
|
93
|
+
if (cleaned && cleaned !== '/' && !cleaned.startsWith('@')) {
|
|
94
|
+
docLines.unshift(cleaned);
|
|
95
|
+
}
|
|
96
|
+
i--;
|
|
97
|
+
}
|
|
98
|
+
if (i >= 0 && lines[i].trim().startsWith('/**')) {
|
|
99
|
+
return docLines.join(' ');
|
|
100
|
+
}
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getJsDocForLine(source: string, lineIdx: number): string {
|
|
105
|
+
return extractJSDoc(source, lineIdx);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function snakeCase(str: string): string {
|
|
109
|
+
return str
|
|
110
|
+
.replace(/([A-Z])/g, '_$1')
|
|
111
|
+
.toLowerCase()
|
|
112
|
+
.replace(/^_/, '')
|
|
113
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
114
|
+
.replace(/_+/g, '_')
|
|
115
|
+
.replace(/^_|_$/g, '');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function inferTsTypeFromValue(val: string): string {
|
|
119
|
+
if (/^\d+$/.test(val)) return 'number';
|
|
120
|
+
if (/^(true|false)$/.test(val)) return 'boolean';
|
|
121
|
+
return 'string';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function paramFromTsSignature(param: string): ToolParameter {
|
|
125
|
+
const parts = param.trim().split(':');
|
|
126
|
+
const name = parts[0].trim().replace(/^\?/, '');
|
|
127
|
+
const required = !param.trim().includes('?');
|
|
128
|
+
let type = 'string';
|
|
129
|
+
let description = '';
|
|
130
|
+
|
|
131
|
+
if (parts.length > 1) {
|
|
132
|
+
const rawType = parts.slice(1).join(':').trim().split('=')[0].trim();
|
|
133
|
+
if (
|
|
134
|
+
rawType === 'string' ||
|
|
135
|
+
rawType === 'number' ||
|
|
136
|
+
rawType === 'boolean' ||
|
|
137
|
+
rawType === 'string[]' ||
|
|
138
|
+
rawType === 'number[]'
|
|
139
|
+
) {
|
|
140
|
+
type = rawType;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
description = type === 'number' ? `Numeric value for ${name}` : `Value for ${name}`;
|
|
145
|
+
return { name, type, required: required && !param.trim().startsWith('?'), description };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sanitizeToolName(raw: string): string {
|
|
149
|
+
return snakeCase(raw)
|
|
150
|
+
.replace(/^get_/, 'get_')
|
|
151
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
152
|
+
.replace(/_+/g, '_')
|
|
153
|
+
.replace(/^_|_$/g, '')
|
|
154
|
+
.substring(0, 64);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Express route detection ──────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function detectExpressRoutes(projectPath: string): ToolCandidate[] {
|
|
160
|
+
const candidates: ToolCandidate[] = [];
|
|
161
|
+
const allTsFiles = walkDir(join(projectPath, 'src'), ['.ts', '.tsx', '.js']);
|
|
162
|
+
const allFiles = [
|
|
163
|
+
...allTsFiles,
|
|
164
|
+
...walkDir(projectPath, ['.ts', '.tsx', '.js']),
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const pkgJsonPath = join(projectPath, 'package.json');
|
|
168
|
+
let isExpressProject = false;
|
|
169
|
+
|
|
170
|
+
if (existsSync(pkgJsonPath)) {
|
|
171
|
+
try {
|
|
172
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
173
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
174
|
+
isExpressProject = 'express' in allDeps;
|
|
175
|
+
} catch {
|
|
176
|
+
// ignore
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const routeFiles = allFiles.filter(
|
|
181
|
+
(f) =>
|
|
182
|
+
f.includes('/routes/') ||
|
|
183
|
+
f.includes('/routers/') ||
|
|
184
|
+
f.includes('router') ||
|
|
185
|
+
(isExpressProject && f.endsWith('.ts')),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
for (const filePath of routeFiles) {
|
|
189
|
+
const content = readFileSafe(filePath);
|
|
190
|
+
if (!content) continue;
|
|
191
|
+
|
|
192
|
+
const routePatterns: RouteMatch[] = [];
|
|
193
|
+
|
|
194
|
+
// app.<method>(path, ...handler)
|
|
195
|
+
const methodRegex =
|
|
196
|
+
/(?:app|router)\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
197
|
+
let m: RegExpExecArray | null;
|
|
198
|
+
while ((m = methodRegex.exec(content)) !== null) {
|
|
199
|
+
routePatterns.push({
|
|
200
|
+
method: m[1].toLowerCase(),
|
|
201
|
+
path: m[2],
|
|
202
|
+
filePath,
|
|
203
|
+
handlerBody: content.substring(m.index, content.indexOf('\n', m.index + 200) || m.index + 200),
|
|
204
|
+
jsdoc: getJsDocForLine(content, content.substring(0, m.index).split('\n').length - 1),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// router.route('/path').get(...)
|
|
209
|
+
const routeRegex =
|
|
210
|
+
/(?:app|router)\s*\.\s*route\s*\(\s*['"`]([^'"`]+)['"`]\)\s*\.\s*(get|post|put|patch|delete)\s*\(/gi;
|
|
211
|
+
while ((m = routeRegex.exec(content)) !== null) {
|
|
212
|
+
routePatterns.push({
|
|
213
|
+
method: m[2].toLowerCase(),
|
|
214
|
+
path: m[1],
|
|
215
|
+
filePath,
|
|
216
|
+
handlerBody: content.substring(m.index, content.indexOf('\n', m.index + 200) || m.index + 200),
|
|
217
|
+
jsdoc: getJsDocForLine(content, content.substring(0, m.index).split('\n').length - 1),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const r of routePatterns) {
|
|
222
|
+
const params = extractRouteParams(r.path, r.handlerBody, r.jsdoc);
|
|
223
|
+
const toolName = generateRouteToolName(r.method, r.path, basename(filePath));
|
|
224
|
+
|
|
225
|
+
candidates.push({
|
|
226
|
+
name: toolName,
|
|
227
|
+
description: r.jsdoc || `${r.method.toUpperCase()} ${r.path}`,
|
|
228
|
+
source: 'express-route',
|
|
229
|
+
filePath,
|
|
230
|
+
parameters: params,
|
|
231
|
+
httpMethod: r.method,
|
|
232
|
+
routePath: r.path,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return candidates;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function extractRouteParams(
|
|
241
|
+
routePath: string,
|
|
242
|
+
handlerBody: string,
|
|
243
|
+
jsdoc: string,
|
|
244
|
+
): ToolParameter[] {
|
|
245
|
+
const params: ToolParameter[] = [];
|
|
246
|
+
|
|
247
|
+
// Extract :param from route
|
|
248
|
+
const pathParams = routePath.match(/:(\w+)/g);
|
|
249
|
+
if (pathParams) {
|
|
250
|
+
for (const p of pathParams) {
|
|
251
|
+
const name = p.substring(1);
|
|
252
|
+
params.push({ name, type: 'string', required: true, description: `Route parameter: ${name}` });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Extract req.query.X from handler body
|
|
257
|
+
const queryRegex = /req\s*\.\s*query\s*\.\s*(\w+)/g;
|
|
258
|
+
let m: RegExpExecArray | null;
|
|
259
|
+
while ((m = queryRegex.exec(handlerBody)) !== null) {
|
|
260
|
+
if (!params.find((p) => p.name === m![1])) {
|
|
261
|
+
params.push({
|
|
262
|
+
name: m[1],
|
|
263
|
+
type: 'string',
|
|
264
|
+
required: false,
|
|
265
|
+
description: `Query parameter: ${m[1]}`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Extract req.body.X from handler body
|
|
271
|
+
const bodyRegex = /req\s*\.\s*body\s*\.\s*(\w+)/g;
|
|
272
|
+
while ((m = bodyRegex.exec(handlerBody)) !== null) {
|
|
273
|
+
if (!params.find((p) => p.name === m![1])) {
|
|
274
|
+
params.push({
|
|
275
|
+
name: m[1],
|
|
276
|
+
type: 'string',
|
|
277
|
+
required: false,
|
|
278
|
+
description: `Request body field: ${m[1]}`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return params;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function generateRouteToolName(method: string, routePath: string, fileName: string): string {
|
|
287
|
+
const parts = routePath.split('/').filter(Boolean);
|
|
288
|
+
const resource = parts.find((p) => !p.startsWith(':') && !p.startsWith('['));
|
|
289
|
+
const baseName = resource || basename(fileName, extname(fileName));
|
|
290
|
+
|
|
291
|
+
const prefixMap: Record<string, string> = {
|
|
292
|
+
get: 'get',
|
|
293
|
+
post: 'create',
|
|
294
|
+
put: 'update',
|
|
295
|
+
patch: 'update',
|
|
296
|
+
delete: 'delete',
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const prefix = prefixMap[method] || method;
|
|
300
|
+
const hasIdParam = parts.some((p) => p.startsWith(':') || p.startsWith('['));
|
|
301
|
+
|
|
302
|
+
if (hasIdParam) {
|
|
303
|
+
return sanitizeToolName(`${prefix}_${baseName}`);
|
|
304
|
+
}
|
|
305
|
+
if (method === 'get') {
|
|
306
|
+
return sanitizeToolName(`list_${baseName}`);
|
|
307
|
+
}
|
|
308
|
+
return sanitizeToolName(`${prefix}_${baseName}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── Next.js API route detection ──────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
function detectNextApiRoutes(projectPath: string): ToolCandidate[] {
|
|
314
|
+
const candidates: ToolCandidate[] = [];
|
|
315
|
+
const appApiDir = join(projectPath, 'app', 'api');
|
|
316
|
+
const pagesApiDir = join(projectPath, 'pages', 'api');
|
|
317
|
+
|
|
318
|
+
// App Router: app/api/**/route.ts
|
|
319
|
+
const appRoutes = walkDir(appApiDir, ['.ts', '.tsx']);
|
|
320
|
+
// Pages Router: pages/api/**/*.ts
|
|
321
|
+
const pagesRoutes = walkDir(pagesApiDir, ['.ts', '.tsx']);
|
|
322
|
+
|
|
323
|
+
const allRouteFiles = [...new Set([...appRoutes, ...pagesRoutes])];
|
|
324
|
+
|
|
325
|
+
for (const filePath of allRouteFiles) {
|
|
326
|
+
const content = readFileSafe(filePath);
|
|
327
|
+
if (!content) continue;
|
|
328
|
+
|
|
329
|
+
const isAppRouter = filePath.includes('/app/api/');
|
|
330
|
+
const relativePath = isAppRouter
|
|
331
|
+
? relative(join(projectPath, 'app', 'api'), filePath).replace(/\/route\.(ts|tsx)$/, '')
|
|
332
|
+
: relative(join(projectPath, 'pages', 'api'), filePath).replace(/\.(ts|tsx)$/, '');
|
|
333
|
+
|
|
334
|
+
const segments = relativePath.split('/').filter(Boolean);
|
|
335
|
+
const patterns: RouteMatch[] = [];
|
|
336
|
+
|
|
337
|
+
if (isAppRouter) {
|
|
338
|
+
// App Router: export async function GET/POST/PUT/PATCH/DELETE
|
|
339
|
+
const methodExportRegex =
|
|
340
|
+
/export\s+async\s+function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/gi;
|
|
341
|
+
let m: RegExpExecArray | null;
|
|
342
|
+
while ((m = methodExportRegex.exec(content)) !== null) {
|
|
343
|
+
const method = m[1].toLowerCase();
|
|
344
|
+
const lineIdx = content.substring(0, m.index).split('\n').length - 1;
|
|
345
|
+
patterns.push({
|
|
346
|
+
method,
|
|
347
|
+
path: `/${segments.join('/')}`,
|
|
348
|
+
filePath,
|
|
349
|
+
handlerBody: content.substring(
|
|
350
|
+
m.index,
|
|
351
|
+
content.indexOf('\n', m.index + 500) || m.index + 500,
|
|
352
|
+
),
|
|
353
|
+
jsdoc: getJsDocForLine(content, lineIdx),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
// Pages Router: export default function handler(req, res)
|
|
358
|
+
const handlerRegex = /export\s+default\s+(?:async\s+)?function\s+\w*\s*\(/gi;
|
|
359
|
+
let m: RegExpExecArray | null;
|
|
360
|
+
while ((m = handlerRegex.exec(content)) !== null) {
|
|
361
|
+
const handlerBody = content.substring(
|
|
362
|
+
m.index,
|
|
363
|
+
content.indexOf('\n', m.index + 1000) || m.index + 1000,
|
|
364
|
+
);
|
|
365
|
+
// Detect method from req.method branching
|
|
366
|
+
const methods = detectPagesMethods(handlerBody);
|
|
367
|
+
for (const method of methods) {
|
|
368
|
+
patterns.push({
|
|
369
|
+
method,
|
|
370
|
+
path: `/${segments.join('/')}`,
|
|
371
|
+
filePath,
|
|
372
|
+
handlerBody,
|
|
373
|
+
jsdoc: getJsDocForLine(content, content.substring(0, m.index).split('\n').length - 1),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const r of patterns) {
|
|
380
|
+
const params = extractNextParams(segments, r.handlerBody, r.jsdoc);
|
|
381
|
+
const fullPath = buildNextPath(segments);
|
|
382
|
+
const toolName = generateRouteToolName(r.method, fullPath, basename(filePath));
|
|
383
|
+
|
|
384
|
+
candidates.push({
|
|
385
|
+
name: toolName,
|
|
386
|
+
description: r.jsdoc || `${r.method.toUpperCase()} /api/${segments.join('/')}`,
|
|
387
|
+
source: 'next-api',
|
|
388
|
+
filePath,
|
|
389
|
+
parameters: params,
|
|
390
|
+
httpMethod: r.method,
|
|
391
|
+
routePath: `/api/${segments.join('/')}`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return candidates;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function detectPagesMethods(handlerBody: string): string[] {
|
|
400
|
+
const methods: string[] = [];
|
|
401
|
+
if (/req\.method\s*===?\s*['"]GET['"]/i.test(handlerBody) || handlerBody.includes('switch')) {
|
|
402
|
+
methods.push('get');
|
|
403
|
+
}
|
|
404
|
+
if (/req\.method\s*===?\s*['"]POST['"]/i.test(handlerBody)) methods.push('post');
|
|
405
|
+
if (/req\.method\s*===?\s*['"]PUT['"]/i.test(handlerBody)) methods.push('put');
|
|
406
|
+
if (/req\.method\s*===?\s*['"]PATCH['"]/i.test(handlerBody)) methods.push('patch');
|
|
407
|
+
if (/req\.method\s*===?\s*['"]DELETE['"]/i.test(handlerBody)) methods.push('delete');
|
|
408
|
+
|
|
409
|
+
if (methods.length === 0) {
|
|
410
|
+
methods.push('get');
|
|
411
|
+
}
|
|
412
|
+
return methods;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function extractNextParams(
|
|
416
|
+
segments: string[],
|
|
417
|
+
handlerBody: string,
|
|
418
|
+
_jsdoc: string,
|
|
419
|
+
): ToolParameter[] {
|
|
420
|
+
const params: ToolParameter[] = [];
|
|
421
|
+
|
|
422
|
+
for (const seg of segments) {
|
|
423
|
+
// [...slug] or [param] or [[...param]]
|
|
424
|
+
const paramMatch = seg.match(/^\[(?:\.{3})?(\w+)\]$/);
|
|
425
|
+
if (paramMatch) {
|
|
426
|
+
const name = paramMatch[1];
|
|
427
|
+
const type = name.endsWith('Id') || name.endsWith('ID') || name.endsWith('id')
|
|
428
|
+
? 'string'
|
|
429
|
+
: 'string';
|
|
430
|
+
params.push({
|
|
431
|
+
name: sanatizeParamName(name),
|
|
432
|
+
type,
|
|
433
|
+
required: !seg.startsWith('[['),
|
|
434
|
+
description: `Route parameter: ${name}`,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return params;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function sanatizeParamName(name: string): string {
|
|
443
|
+
return name === 'id' || name === 'ID' || name === 'Id' ? 'id' : snakeCase(name);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function buildNextPath(segments: string[]): string {
|
|
447
|
+
return '/' + segments.map((s) => (s.startsWith('[') ? `:${s.replace(/[\[\]]/g, '')}` : s)).join('/');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ─── CLI script detection ─────────────────────────────────────────────────────
|
|
451
|
+
|
|
452
|
+
function detectCliScripts(projectPath: string): ToolCandidate[] {
|
|
453
|
+
const candidates: ToolCandidate[] = [];
|
|
454
|
+
const pkgJsonPath = join(projectPath, 'package.json');
|
|
455
|
+
|
|
456
|
+
// Parse package.json bin field
|
|
457
|
+
if (existsSync(pkgJsonPath)) {
|
|
458
|
+
try {
|
|
459
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
460
|
+
if (pkg.bin) {
|
|
461
|
+
const bins = typeof pkg.bin === 'string' ? { [pkg.name]: pkg.bin } : pkg.bin;
|
|
462
|
+
for (const [binName, binPath] of Object.entries(bins) as [string, string][]) {
|
|
463
|
+
const fullPath = resolve(projectPath, binPath);
|
|
464
|
+
if (existsSync(fullPath)) {
|
|
465
|
+
const content = readFileSafe(fullPath);
|
|
466
|
+
const params = extractCliParams(content);
|
|
467
|
+
const description =
|
|
468
|
+
getJsDocForLine(content, 0) || `${binName} CLI command`;
|
|
469
|
+
candidates.push({
|
|
470
|
+
name: sanitizeToolName(`run_${binName}`),
|
|
471
|
+
description:
|
|
472
|
+
pkg.description || description,
|
|
473
|
+
source: 'cli-script',
|
|
474
|
+
filePath: fullPath,
|
|
475
|
+
parameters: params,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Parse scripts field too
|
|
482
|
+
if (pkg.scripts) {
|
|
483
|
+
for (const [scriptName, scriptCmd] of Object.entries(pkg.scripts) as [string, string][]) {
|
|
484
|
+
const params = extractScriptParams(scriptCmd);
|
|
485
|
+
candidates.push({
|
|
486
|
+
name: sanitizeToolName(`run_${scriptName}`),
|
|
487
|
+
description: `Run npm script: ${scriptName} — ${scriptCmd}`,
|
|
488
|
+
source: 'cli-script',
|
|
489
|
+
filePath: pkgJsonPath,
|
|
490
|
+
parameters: params,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch {
|
|
495
|
+
// ignore
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Scan bin/ and scripts/ directories
|
|
500
|
+
for (const dir of ['bin', 'scripts']) {
|
|
501
|
+
const dirPath = join(projectPath, dir);
|
|
502
|
+
if (!existsSync(dirPath)) continue;
|
|
503
|
+
|
|
504
|
+
const scriptFiles = walkDir(dirPath, ['.ts', '.js', '.sh', '.mjs', '.cjs']);
|
|
505
|
+
for (const filePath of scriptFiles) {
|
|
506
|
+
const fname = basename(filePath, extname(filePath));
|
|
507
|
+
const content = readFileSafe(filePath);
|
|
508
|
+
const params = extractCliParams(content);
|
|
509
|
+
|
|
510
|
+
candidates.push({
|
|
511
|
+
name: sanitizeToolName(`run_${fname}`),
|
|
512
|
+
description: `CLI script: ${fname}`,
|
|
513
|
+
source: 'cli-script',
|
|
514
|
+
filePath,
|
|
515
|
+
parameters: params,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return candidates;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function extractCliParams(content: string): ToolParameter[] {
|
|
524
|
+
const params: ToolParameter[] = [];
|
|
525
|
+
// commander/yargs style: .option('--name <type>', 'desc')
|
|
526
|
+
const optionRegex = /\.(?:option|requiredOption)\s*\(\s*['"](--?\w+)\s*(?:<(\w+)>)?['"],\s*['"]([^'"]*)['"]/g;
|
|
527
|
+
let m: RegExpExecArray | null;
|
|
528
|
+
while ((m = optionRegex.exec(content)) !== null) {
|
|
529
|
+
const flag = m[1].replace(/^--?/, '');
|
|
530
|
+
const type = m[2] || 'string';
|
|
531
|
+
params.push({
|
|
532
|
+
name: flag,
|
|
533
|
+
type: type === 'number' ? 'number' : 'string',
|
|
534
|
+
required: content.includes('requiredOption') || false,
|
|
535
|
+
description: m[3] || `CLI flag: ${m[1]}`,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// process.argv style
|
|
540
|
+
const argRegex = /process\.argv\[\s*(\d+)\s*\]/g;
|
|
541
|
+
let posFound = false;
|
|
542
|
+
while ((m = argRegex.exec(content)) !== null) {
|
|
543
|
+
posFound = true;
|
|
544
|
+
}
|
|
545
|
+
if (posFound && params.length === 0) {
|
|
546
|
+
params.push({
|
|
547
|
+
name: 'args',
|
|
548
|
+
type: 'string',
|
|
549
|
+
required: false,
|
|
550
|
+
description: 'Command arguments',
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return params;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function extractScriptParams(scriptCmd: string): ToolParameter[] {
|
|
558
|
+
const params: ToolParameter[] = [];
|
|
559
|
+
// Look for environment variable references in script commands
|
|
560
|
+
const envRegex = /\$\{?(\w+)\}?/g;
|
|
561
|
+
let m: RegExpExecArray | null;
|
|
562
|
+
while ((m = envRegex.exec(scriptCmd)) !== null) {
|
|
563
|
+
const envName = m[1];
|
|
564
|
+
if (!/^(npm_|npm_config|PATH|HOME|USER|NODE)/.test(envName)) {
|
|
565
|
+
params.push({
|
|
566
|
+
name: snakeCase(envName),
|
|
567
|
+
type: 'string',
|
|
568
|
+
required: false,
|
|
569
|
+
description: `Environment variable: ${envName}`,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return params;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ─── TypeScript exported async function detection ─────────────────────────────
|
|
577
|
+
|
|
578
|
+
function detectExportedFunctions(projectPath: string): ToolCandidate[] {
|
|
579
|
+
const candidates: ToolCandidate[] = [];
|
|
580
|
+
const allTsFiles = walkDir(join(projectPath, 'src'), ['.ts', '.tsx']);
|
|
581
|
+
const extraFiles = walkDir(join(projectPath, 'lib'), ['.ts', '.tsx']);
|
|
582
|
+
|
|
583
|
+
const files = [...new Set([...allTsFiles, ...extraFiles])];
|
|
584
|
+
|
|
585
|
+
for (const filePath of files) {
|
|
586
|
+
const content = readFileSafe(filePath);
|
|
587
|
+
if (!content || content.length > 200_000) continue;
|
|
588
|
+
|
|
589
|
+
// Match: export async function name(args): ReturnType {
|
|
590
|
+
const funcRegex =
|
|
591
|
+
/export\s+async\s+function\s+(\w+)\s*\(\s*([^)]*)\)\s*(?::\s*([^{]+?))?\s*\{/g;
|
|
592
|
+
let m: RegExpExecArray | null;
|
|
593
|
+
while ((m = funcRegex.exec(content)) !== null) {
|
|
594
|
+
const funcName = m[1];
|
|
595
|
+
const rawParams = m[2] || '';
|
|
596
|
+
const returnType = (m[3] || '').trim();
|
|
597
|
+
const lineIdx = content.substring(0, m.index).split('\n').length - 1;
|
|
598
|
+
const jsdoc = getJsDocForLine(content, lineIdx);
|
|
599
|
+
|
|
600
|
+
const params = parseTsParams(rawParams);
|
|
601
|
+
|
|
602
|
+
// Skip internal/private-looking functions
|
|
603
|
+
if (funcName.startsWith('_') || funcName.startsWith('#')) continue;
|
|
604
|
+
// Skip React components
|
|
605
|
+
if (/^[A-Z]/.test(funcName) && !returnType.includes('Promise')) continue;
|
|
606
|
+
|
|
607
|
+
const toolName = sanitizeToolName(funcName);
|
|
608
|
+
const description =
|
|
609
|
+
jsdoc ||
|
|
610
|
+
(returnType
|
|
611
|
+
? `Calls ${funcName} (returns ${returnType})`
|
|
612
|
+
: `Calls ${funcName}`);
|
|
613
|
+
|
|
614
|
+
candidates.push({
|
|
615
|
+
name: toolName,
|
|
616
|
+
description,
|
|
617
|
+
source: 'exported-function',
|
|
618
|
+
filePath,
|
|
619
|
+
parameters: params,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return deduplicateTools(candidates);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function parseTsParams(rawParams: string): ToolParameter[] {
|
|
628
|
+
if (!rawParams.trim()) return [];
|
|
629
|
+
|
|
630
|
+
const params: ToolParameter[] = [];
|
|
631
|
+
let depth = 0;
|
|
632
|
+
let current = '';
|
|
633
|
+
|
|
634
|
+
for (const ch of rawParams) {
|
|
635
|
+
if (ch === '<' || ch === '(' || ch === '{' || ch === '[') depth++;
|
|
636
|
+
else if (ch === '>' || ch === ')' || ch === '}' || ch === ']') depth--;
|
|
637
|
+
else if (ch === ',' && depth === 0) {
|
|
638
|
+
if (current.trim()) {
|
|
639
|
+
params.push(paramFromTsSignature(current.trim()));
|
|
640
|
+
}
|
|
641
|
+
current = '';
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
current += ch;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (current.trim()) {
|
|
648
|
+
params.push(paramFromTsSignature(current.trim()));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return params;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function deduplicateTools(candidates: ToolCandidate[]): ToolCandidate[] {
|
|
655
|
+
const seen = new Map<string, ToolCandidate>();
|
|
656
|
+
for (const c of candidates) {
|
|
657
|
+
if (!seen.has(c.name)) {
|
|
658
|
+
seen.set(c.name, c);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return Array.from(seen.values());
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ─── Python FastAPI detection ─────────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
function detectFastApiRoutes(projectPath: string): ToolCandidate[] {
|
|
667
|
+
const candidates: ToolCandidate[] = [];
|
|
668
|
+
const pyFiles = walkDir(projectPath, ['.py']);
|
|
669
|
+
|
|
670
|
+
for (const filePath of pyFiles) {
|
|
671
|
+
const content = readFileSafe(filePath);
|
|
672
|
+
if (!content) continue;
|
|
673
|
+
|
|
674
|
+
// @app.get('/path') or @router.get('/path')
|
|
675
|
+
const decoratorRegex =
|
|
676
|
+
/@(?:app|router)\s*\.\s*(get|post|put|patch|delete|options)\s*\(\s*['"]([^'"']+)['"]/gi;
|
|
677
|
+
let m: RegExpExecArray | null;
|
|
678
|
+
while ((m = decoratorRegex.exec(content)) !== null) {
|
|
679
|
+
const method = m[1].toLowerCase();
|
|
680
|
+
const routePath = m[2];
|
|
681
|
+
const lineIdx = content.substring(0, m.index).split('\n').length - 1;
|
|
682
|
+
|
|
683
|
+
// Get the function line that follows
|
|
684
|
+
const afterDecorator = content.substring(m.index + m[0].length);
|
|
685
|
+
const funcMatch = afterDecorator.match(/def\s+(\w+)\s*\(/);
|
|
686
|
+
const funcName = funcMatch ? funcMatch[1] : 'handler';
|
|
687
|
+
|
|
688
|
+
// Get the function body for parameter detection
|
|
689
|
+
const restOfFile = content.substring(m.index);
|
|
690
|
+
const bodyMatch = restOfFile.match(/def\s+\w+\s*\(([^)]*)\)/) || [''];
|
|
691
|
+
const paramStr = bodyMatch[1] || '';
|
|
692
|
+
|
|
693
|
+
const params = parsePythonParams(paramStr, routePath);
|
|
694
|
+
|
|
695
|
+
const toolName = generateRouteToolName(method, routePath, funcName);
|
|
696
|
+
|
|
697
|
+
candidates.push({
|
|
698
|
+
name: toolName,
|
|
699
|
+
description: `${method.toUpperCase()} ${routePath} (${funcName})`,
|
|
700
|
+
source: 'fastapi-route',
|
|
701
|
+
filePath,
|
|
702
|
+
parameters: params,
|
|
703
|
+
httpMethod: method,
|
|
704
|
+
routePath,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return candidates;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function parsePythonParams(paramStr: string, routePath: string): ToolParameter[] {
|
|
713
|
+
const params: ToolParameter[] = [];
|
|
714
|
+
|
|
715
|
+
// Extract route params
|
|
716
|
+
const pathParamRegex = /\{(\w+)\}/g;
|
|
717
|
+
let m: RegExpExecArray | null;
|
|
718
|
+
while ((m = pathParamRegex.exec(routePath)) !== null) {
|
|
719
|
+
params.push({
|
|
720
|
+
name: m[1],
|
|
721
|
+
type: 'string',
|
|
722
|
+
required: true,
|
|
723
|
+
description: `Path parameter: ${m[1]}`,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Extract function params that are not 'self', 'request', 'response'
|
|
728
|
+
const funcParams = paramStr
|
|
729
|
+
.split(',')
|
|
730
|
+
.map((p) => p.trim())
|
|
731
|
+
.filter((p) => p && p !== 'self' && p !== 'request' && p !== 'response');
|
|
732
|
+
|
|
733
|
+
for (const p of funcParams) {
|
|
734
|
+
const parts = p.split(':');
|
|
735
|
+
const name = parts[0].trim();
|
|
736
|
+
const type = parts.length > 1 ? parts[1].trim().split('=')[0].trim() : 'str';
|
|
737
|
+
const hasDefault = p.includes('=');
|
|
738
|
+
|
|
739
|
+
const tsType = mapPythonTypeToTs(type);
|
|
740
|
+
|
|
741
|
+
if (!params.find((x) => x.name === name)) {
|
|
742
|
+
params.push({
|
|
743
|
+
name,
|
|
744
|
+
type: tsType,
|
|
745
|
+
required: !hasDefault,
|
|
746
|
+
description: `Function parameter: ${name}`,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return params;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function mapPythonTypeToTs(pyType: string): string {
|
|
755
|
+
const typeMap: Record<string, string> = {
|
|
756
|
+
int: 'number',
|
|
757
|
+
float: 'number',
|
|
758
|
+
str: 'string',
|
|
759
|
+
bool: 'boolean',
|
|
760
|
+
list: 'string[]',
|
|
761
|
+
dict: 'object',
|
|
762
|
+
None: 'string',
|
|
763
|
+
};
|
|
764
|
+
return typeMap[pyType] || 'string';
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ─── Main detection ───────────────────────────────────────────────────────────
|
|
768
|
+
|
|
769
|
+
export async function detectToolCandidates(
|
|
770
|
+
projectPath: string,
|
|
771
|
+
): Promise<ToolCandidate[]> {
|
|
772
|
+
const absPath = resolve(projectPath);
|
|
773
|
+
if (!existsSync(absPath)) {
|
|
774
|
+
throw new Error(`Project path does not exist: ${absPath}`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const isTypescript = existsSync(join(absPath, 'tsconfig.json'));
|
|
778
|
+
const isPython = !isTypescript && walkDir(absPath, ['.py']).length > 0;
|
|
779
|
+
|
|
780
|
+
let allCandidates: ToolCandidate[] = [];
|
|
781
|
+
|
|
782
|
+
if (isTypescript) {
|
|
783
|
+
allCandidates = [
|
|
784
|
+
...detectExpressRoutes(absPath),
|
|
785
|
+
...detectNextApiRoutes(absPath),
|
|
786
|
+
...detectCliScripts(absPath),
|
|
787
|
+
...detectExportedFunctions(absPath),
|
|
788
|
+
];
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (isPython) {
|
|
792
|
+
allCandidates = [...detectFastApiRoutes(absPath), ...detectCliScripts(absPath)];
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Fallback: scan both
|
|
796
|
+
if (!isTypescript && !isPython) {
|
|
797
|
+
allCandidates = [
|
|
798
|
+
...detectExpressRoutes(absPath),
|
|
799
|
+
...detectNextApiRoutes(absPath),
|
|
800
|
+
...detectFastApiRoutes(absPath),
|
|
801
|
+
...detectCliScripts(absPath),
|
|
802
|
+
...detectExportedFunctions(absPath),
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return deduplicateTools(allCandidates);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ─── MCP Server Code Generation ───────────────────────────────────────────────
|
|
810
|
+
|
|
811
|
+
function generateServerCode(tools: ToolCandidate[]): string {
|
|
812
|
+
const toolDefinitions = tools
|
|
813
|
+
.map((t) => {
|
|
814
|
+
const inputSchema = generateInputSchema(t.parameters);
|
|
815
|
+
return ` {
|
|
816
|
+
name: ${JSON.stringify(t.name)},
|
|
817
|
+
description: ${JSON.stringify(t.description)},
|
|
818
|
+
inputSchema: ${JSON.stringify(inputSchema, null, 6).replace(/\n/g, '\n ')},
|
|
819
|
+
}`;
|
|
820
|
+
})
|
|
821
|
+
.join(',\n');
|
|
822
|
+
|
|
823
|
+
const toolHandlers = tools
|
|
824
|
+
.map((t) => {
|
|
825
|
+
const paramExtract = t.parameters
|
|
826
|
+
.map((p) => ` const ${p.name} = args.${p.name};`)
|
|
827
|
+
.join('\n');
|
|
828
|
+
|
|
829
|
+
return ` case '${t.name}':
|
|
830
|
+
${t.parameters.length > 0 ? paramExtract + '\n' : ''} // Source: ${t.filePath}
|
|
831
|
+
return {
|
|
832
|
+
content: [{ type: 'text', text: JSON.stringify({ tool: '${t.name}', status: 'ok', args, message: 'Tool ${t.name} invoked. Implement handler at ${t.filePath}' }, null, 2) }],
|
|
833
|
+
};`;
|
|
834
|
+
})
|
|
835
|
+
.join('\n\n');
|
|
836
|
+
|
|
837
|
+
const isTypescript = tools.some((t) => t.source !== 'fastapi-route');
|
|
838
|
+
|
|
839
|
+
return `#!/usr/bin/env node
|
|
840
|
+
/**
|
|
841
|
+
* Auto-generated MCP Server
|
|
842
|
+
* Generated by @aria/connector auto-mcp
|
|
843
|
+
*
|
|
844
|
+
* Tools detected: ${tools.length}
|
|
845
|
+
* Sources: ${[...new Set(tools.map((t) => t.source))].join(', ')}
|
|
846
|
+
*/
|
|
847
|
+
${isTypescript ? `import process from 'node:process';` : ''}
|
|
848
|
+
${isTypescript ? `import { readFileSync } from 'node:fs';` : ''}
|
|
849
|
+
${isTypescript ? `import { resolve, dirname } from 'node:path';` : ''}
|
|
850
|
+
${isTypescript ? `import { fileURLToPath } from 'node:url';` : ''}
|
|
851
|
+
|
|
852
|
+
${isTypescript ? `const __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);` : ''}
|
|
853
|
+
|
|
854
|
+
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
855
|
+
|
|
856
|
+
const TOOLS = [
|
|
857
|
+
${toolDefinitions}
|
|
858
|
+
];
|
|
859
|
+
|
|
860
|
+
// ── Tool handlers ────────────────────────────────────────────────────────────
|
|
861
|
+
|
|
862
|
+
async function callTool(name, args) {
|
|
863
|
+
switch (name) {
|
|
864
|
+
${toolHandlers}
|
|
865
|
+
|
|
866
|
+
default:
|
|
867
|
+
return {
|
|
868
|
+
content: [{ type: 'text', text: JSON.stringify({ error: \`Unknown tool: \${name}\` }) }],
|
|
869
|
+
isError: true,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ── MCP stdio protocol ──────────────────────────────────────────────────────
|
|
875
|
+
|
|
876
|
+
interface McpRequest {
|
|
877
|
+
jsonrpc: '2.0';
|
|
878
|
+
id?: number | string;
|
|
879
|
+
method: string;
|
|
880
|
+
params?: Record<string, unknown>;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
interface McpResponse {
|
|
884
|
+
jsonrpc: '2.0';
|
|
885
|
+
id?: number | string;
|
|
886
|
+
result?: unknown;
|
|
887
|
+
error?: { code: number; message: string; data?: unknown };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function sendResponse(response: McpResponse): void {
|
|
891
|
+
process.stdout.write(JSON.stringify(response) + '\\n');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function sendNotification(method: string, params: Record<string, unknown>): void {
|
|
895
|
+
const notification = {
|
|
896
|
+
jsonrpc: '2.0' as const,
|
|
897
|
+
method,
|
|
898
|
+
params,
|
|
899
|
+
};
|
|
900
|
+
process.stdout.write(JSON.stringify(notification) + '\\n');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
async function handleRequest(req: McpRequest): Promise<void> {
|
|
904
|
+
try {
|
|
905
|
+
switch (req.method) {
|
|
906
|
+
case 'initialize':
|
|
907
|
+
sendResponse({
|
|
908
|
+
jsonrpc: '2.0',
|
|
909
|
+
id: req.id,
|
|
910
|
+
result: {
|
|
911
|
+
protocolVersion: '2024-11-05',
|
|
912
|
+
serverInfo: {
|
|
913
|
+
name: 'auto-mcp-server',
|
|
914
|
+
version: '0.1.0',
|
|
915
|
+
},
|
|
916
|
+
capabilities: {
|
|
917
|
+
tools: {},
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
});
|
|
921
|
+
break;
|
|
922
|
+
|
|
923
|
+
case 'tools/list':
|
|
924
|
+
sendResponse({
|
|
925
|
+
jsonrpc: '2.0',
|
|
926
|
+
id: req.id,
|
|
927
|
+
result: { tools: TOOLS },
|
|
928
|
+
});
|
|
929
|
+
break;
|
|
930
|
+
|
|
931
|
+
case 'tools/call':
|
|
932
|
+
const result = await callTool(
|
|
933
|
+
(req.params as Record<string, string>)?.name,
|
|
934
|
+
(req.params as Record<string, unknown>)?.arguments || {},
|
|
935
|
+
);
|
|
936
|
+
sendResponse({
|
|
937
|
+
jsonrpc: '2.0',
|
|
938
|
+
id: req.id,
|
|
939
|
+
result,
|
|
940
|
+
});
|
|
941
|
+
break;
|
|
942
|
+
|
|
943
|
+
case 'notifications/initialized':
|
|
944
|
+
void 0;
|
|
945
|
+
break;
|
|
946
|
+
|
|
947
|
+
case 'ping':
|
|
948
|
+
sendResponse({
|
|
949
|
+
jsonrpc: '2.0',
|
|
950
|
+
id: req.id,
|
|
951
|
+
result: {},
|
|
952
|
+
});
|
|
953
|
+
break;
|
|
954
|
+
|
|
955
|
+
default:
|
|
956
|
+
sendResponse({
|
|
957
|
+
jsonrpc: '2.0',
|
|
958
|
+
id: req.id,
|
|
959
|
+
error: {
|
|
960
|
+
code: -32601,
|
|
961
|
+
message: \`Method not found: \${req.method}\`,
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
} catch (error) {
|
|
966
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
967
|
+
sendResponse({
|
|
968
|
+
jsonrpc: '2.0',
|
|
969
|
+
id: req.id,
|
|
970
|
+
error: {
|
|
971
|
+
code: -32603,
|
|
972
|
+
message,
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function main(): void {
|
|
979
|
+
let buffer = '';
|
|
980
|
+
|
|
981
|
+
process.stdin.setEncoding('utf-8');
|
|
982
|
+
process.stdin.on('data', (chunk: string) => {
|
|
983
|
+
buffer += chunk;
|
|
984
|
+
|
|
985
|
+
let newlineIdx: number;
|
|
986
|
+
while ((newlineIdx = buffer.indexOf('\\n')) !== -1) {
|
|
987
|
+
const line = buffer.substring(0, newlineIdx).trim();
|
|
988
|
+
buffer = buffer.substring(newlineIdx + 1);
|
|
989
|
+
|
|
990
|
+
if (!line) continue;
|
|
991
|
+
|
|
992
|
+
try {
|
|
993
|
+
const req = JSON.parse(line) as McpRequest;
|
|
994
|
+
handleRequest(req);
|
|
995
|
+
} catch {
|
|
996
|
+
sendResponse({
|
|
997
|
+
jsonrpc: '2.0',
|
|
998
|
+
id: undefined,
|
|
999
|
+
error: { code: -32700, message: 'Parse error' },
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
process.stdin.on('end', () => {
|
|
1006
|
+
process.exit(0);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
process.on('uncaughtException', (error: Error) => {
|
|
1010
|
+
console.error('Uncaught exception:', error.message);
|
|
1011
|
+
process.exit(1);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
process.on('unhandledRejection', (reason: unknown) => {
|
|
1015
|
+
console.error('Unhandled rejection:', reason instanceof Error ? reason.message : String(reason));
|
|
1016
|
+
process.exit(1);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
main();
|
|
1021
|
+
`;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function generateInputSchema(params: ToolParameter[]): Record<string, unknown> {
|
|
1025
|
+
const properties: Record<string, Record<string, unknown>> = {};
|
|
1026
|
+
const required: string[] = [];
|
|
1027
|
+
|
|
1028
|
+
for (const p of params) {
|
|
1029
|
+
properties[p.name] = {
|
|
1030
|
+
type: p.type,
|
|
1031
|
+
description: p.description,
|
|
1032
|
+
};
|
|
1033
|
+
if (p.required) {
|
|
1034
|
+
required.push(p.name);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
return {
|
|
1039
|
+
type: 'object',
|
|
1040
|
+
properties,
|
|
1041
|
+
...(required.length > 0 ? { required } : {}),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function generateReadme(toolCount: number, sourceTypes: string[]): string {
|
|
1046
|
+
const sources = sourceTypes
|
|
1047
|
+
.map((s) => `- ${s}`)
|
|
1048
|
+
.join('\n');
|
|
1049
|
+
|
|
1050
|
+
return `# Auto-MCP Server
|
|
1051
|
+
|
|
1052
|
+
Auto-generated MCP server with ${toolCount} detected tools.
|
|
1053
|
+
|
|
1054
|
+
## Sources Detected
|
|
1055
|
+
|
|
1056
|
+
${sources}
|
|
1057
|
+
|
|
1058
|
+
## Usage
|
|
1059
|
+
|
|
1060
|
+
\`\`\`bash
|
|
1061
|
+
# Install dependencies
|
|
1062
|
+
npm install
|
|
1063
|
+
|
|
1064
|
+
# Build
|
|
1065
|
+
npm run build
|
|
1066
|
+
|
|
1067
|
+
# Run (via stdio)
|
|
1068
|
+
node dist/server.js
|
|
1069
|
+
\`\`\`
|
|
1070
|
+
|
|
1071
|
+
## Adding to Claude Desktop / Cursor / OpenCode
|
|
1072
|
+
|
|
1073
|
+
\`\`\`json
|
|
1074
|
+
{
|
|
1075
|
+
"mcpServers": {
|
|
1076
|
+
"auto-mcp": {
|
|
1077
|
+
"command": "node",
|
|
1078
|
+
"args": ["dist/server.js"],
|
|
1079
|
+
"cwd": "."
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
\`\`\`
|
|
1084
|
+
|
|
1085
|
+
## Tools
|
|
1086
|
+
|
|
1087
|
+
${toolCount} tools available. See \`src/server.ts\` for full tool definitions.
|
|
1088
|
+
|
|
1089
|
+
## Generated by
|
|
1090
|
+
|
|
1091
|
+
@aria/connector auto-mcp generator
|
|
1092
|
+
`;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// ─── Main generation ──────────────────────────────────────────────────────────
|
|
1096
|
+
|
|
1097
|
+
export async function generateMcpServer(
|
|
1098
|
+
projectPath: string,
|
|
1099
|
+
outputPath: string,
|
|
1100
|
+
): Promise<McpGenerationResult> {
|
|
1101
|
+
const absProjectPath = resolve(projectPath);
|
|
1102
|
+
const absOutputPath = resolve(outputPath);
|
|
1103
|
+
|
|
1104
|
+
const tools = await detectToolCandidates(absProjectPath);
|
|
1105
|
+
|
|
1106
|
+
// Create output directory structure
|
|
1107
|
+
const srcDir = join(absOutputPath, 'src');
|
|
1108
|
+
mkdirSync(srcDir, { recursive: true });
|
|
1109
|
+
|
|
1110
|
+
// Write package.json
|
|
1111
|
+
const pkgJson = {
|
|
1112
|
+
name: 'auto-mcp-server',
|
|
1113
|
+
version: '0.1.0',
|
|
1114
|
+
description: `Auto-generated MCP server for ${basename(absProjectPath)}`,
|
|
1115
|
+
type: 'module',
|
|
1116
|
+
main: './dist/src/server.js',
|
|
1117
|
+
types: './dist/src/server.d.ts',
|
|
1118
|
+
scripts: {
|
|
1119
|
+
build: 'tsc',
|
|
1120
|
+
start: 'node dist/src/server.js',
|
|
1121
|
+
dev: 'tsc --watch',
|
|
1122
|
+
},
|
|
1123
|
+
dependencies: {},
|
|
1124
|
+
devDependencies: {
|
|
1125
|
+
'@types/node': '^22.0.0',
|
|
1126
|
+
typescript: '^5.7.0',
|
|
1127
|
+
},
|
|
1128
|
+
engines: {
|
|
1129
|
+
node: '>=20.0.0',
|
|
1130
|
+
},
|
|
1131
|
+
license: 'UNLICENSED',
|
|
1132
|
+
private: true,
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
writeFileSync(join(absOutputPath, 'package.json'), JSON.stringify(pkgJson, null, 2));
|
|
1136
|
+
|
|
1137
|
+
// Write tsconfig.json
|
|
1138
|
+
const tsConfig = {
|
|
1139
|
+
compilerOptions: {
|
|
1140
|
+
target: 'ES2022',
|
|
1141
|
+
module: 'ES2022',
|
|
1142
|
+
moduleResolution: 'bundler',
|
|
1143
|
+
outDir: './dist',
|
|
1144
|
+
strict: true,
|
|
1145
|
+
esModuleInterop: true,
|
|
1146
|
+
skipLibCheck: true,
|
|
1147
|
+
forceConsistentCasingInFileNames: true,
|
|
1148
|
+
declaration: true,
|
|
1149
|
+
sourceMap: true,
|
|
1150
|
+
noEmitOnError: true,
|
|
1151
|
+
},
|
|
1152
|
+
include: ['src/**/*.ts'],
|
|
1153
|
+
exclude: ['node_modules', 'dist'],
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
writeFileSync(join(absOutputPath, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
|
|
1157
|
+
|
|
1158
|
+
// Write server.ts
|
|
1159
|
+
const serverCode = generateServerCode(tools);
|
|
1160
|
+
writeFileSync(join(srcDir, 'server.ts'), serverCode);
|
|
1161
|
+
|
|
1162
|
+
// Write README.md
|
|
1163
|
+
const sourceTypes = [...new Set(tools.map((t) => t.source))];
|
|
1164
|
+
const readme = generateReadme(tools.length, sourceTypes);
|
|
1165
|
+
writeFileSync(join(absOutputPath, 'README.md'), readme);
|
|
1166
|
+
|
|
1167
|
+
return {
|
|
1168
|
+
serverPath: absOutputPath,
|
|
1169
|
+
toolCount: tools.length,
|
|
1170
|
+
tools: tools.map((t) => t.name),
|
|
1171
|
+
};
|
|
1172
|
+
}
|