@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.
Files changed (153) hide show
  1. package/bin/aria.js +168 -0
  2. package/dist/aria-connector/src/auth-commands.d.ts +28 -0
  3. package/dist/aria-connector/src/auth-commands.d.ts.map +1 -0
  4. package/dist/aria-connector/src/auth-commands.js +129 -0
  5. package/dist/aria-connector/src/auth-commands.js.map +1 -0
  6. package/dist/aria-connector/src/auth.d.ts +12 -0
  7. package/dist/aria-connector/src/auth.d.ts.map +1 -0
  8. package/dist/aria-connector/src/auth.js +31 -0
  9. package/dist/aria-connector/src/auth.js.map +1 -0
  10. package/dist/aria-connector/src/auto-mcp.d.ts +23 -0
  11. package/dist/aria-connector/src/auto-mcp.d.ts.map +1 -0
  12. package/dist/aria-connector/src/auto-mcp.js +994 -0
  13. package/dist/aria-connector/src/auto-mcp.js.map +1 -0
  14. package/dist/aria-connector/src/chat.d.ts +21 -0
  15. package/dist/aria-connector/src/chat.d.ts.map +1 -0
  16. package/dist/aria-connector/src/chat.js +332 -0
  17. package/dist/aria-connector/src/chat.js.map +1 -0
  18. package/dist/aria-connector/src/codebase-scanner.d.ts +7 -0
  19. package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -0
  20. package/dist/aria-connector/src/codebase-scanner.js +6 -0
  21. package/dist/aria-connector/src/codebase-scanner.js.map +1 -0
  22. package/dist/aria-connector/src/cognition-log.d.ts +17 -0
  23. package/dist/aria-connector/src/cognition-log.d.ts.map +1 -0
  24. package/dist/aria-connector/src/cognition-log.js +19 -0
  25. package/dist/aria-connector/src/cognition-log.js.map +1 -0
  26. package/dist/aria-connector/src/config.d.ts +41 -0
  27. package/dist/aria-connector/src/config.d.ts.map +1 -0
  28. package/dist/aria-connector/src/config.js +50 -0
  29. package/dist/aria-connector/src/config.js.map +1 -0
  30. package/dist/aria-connector/src/connectors/claude-code.d.ts +4 -0
  31. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -0
  32. package/dist/aria-connector/src/connectors/claude-code.js +204 -0
  33. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -0
  34. package/dist/aria-connector/src/connectors/cursor.d.ts +4 -0
  35. package/dist/aria-connector/src/connectors/cursor.d.ts.map +1 -0
  36. package/dist/aria-connector/src/connectors/cursor.js +63 -0
  37. package/dist/aria-connector/src/connectors/cursor.js.map +1 -0
  38. package/dist/aria-connector/src/connectors/opencode.d.ts +4 -0
  39. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -0
  40. package/dist/aria-connector/src/connectors/opencode.js +102 -0
  41. package/dist/aria-connector/src/connectors/opencode.js.map +1 -0
  42. package/dist/aria-connector/src/connectors/shell.d.ts +4 -0
  43. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -0
  44. package/dist/aria-connector/src/connectors/shell.js +58 -0
  45. package/dist/aria-connector/src/connectors/shell.js.map +1 -0
  46. package/dist/aria-connector/src/garden-client.d.ts +19 -0
  47. package/dist/aria-connector/src/garden-client.d.ts.map +1 -0
  48. package/dist/aria-connector/src/garden-client.js +85 -0
  49. package/dist/aria-connector/src/garden-client.js.map +1 -0
  50. package/dist/aria-connector/src/garden-control-plane.d.ts +22 -0
  51. package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -0
  52. package/dist/aria-connector/src/garden-control-plane.js +43 -0
  53. package/dist/aria-connector/src/garden-control-plane.js.map +1 -0
  54. package/dist/aria-connector/src/harness-client.d.ts +166 -0
  55. package/dist/aria-connector/src/harness-client.d.ts.map +1 -0
  56. package/dist/aria-connector/src/harness-client.js +344 -0
  57. package/dist/aria-connector/src/harness-client.js.map +1 -0
  58. package/dist/aria-connector/src/hive-client.d.ts +32 -0
  59. package/dist/aria-connector/src/hive-client.d.ts.map +1 -0
  60. package/dist/aria-connector/src/hive-client.js +69 -0
  61. package/dist/aria-connector/src/hive-client.js.map +1 -0
  62. package/dist/aria-connector/src/index.d.ts +19 -0
  63. package/dist/aria-connector/src/index.d.ts.map +1 -0
  64. package/dist/aria-connector/src/index.js +13 -0
  65. package/dist/aria-connector/src/index.js.map +1 -0
  66. package/dist/aria-connector/src/install-hooks.d.ts +18 -0
  67. package/dist/aria-connector/src/install-hooks.d.ts.map +1 -0
  68. package/dist/aria-connector/src/install-hooks.js +224 -0
  69. package/dist/aria-connector/src/install-hooks.js.map +1 -0
  70. package/dist/aria-connector/src/model-context.d.ts +8 -0
  71. package/dist/aria-connector/src/model-context.d.ts.map +1 -0
  72. package/dist/aria-connector/src/model-context.js +83 -0
  73. package/dist/aria-connector/src/model-context.js.map +1 -0
  74. package/dist/aria-connector/src/persona.d.ts +27 -0
  75. package/dist/aria-connector/src/persona.d.ts.map +1 -0
  76. package/dist/aria-connector/src/persona.js +86 -0
  77. package/dist/aria-connector/src/persona.js.map +1 -0
  78. package/dist/aria-connector/src/providers/anthropic.d.ts +4 -0
  79. package/dist/aria-connector/src/providers/anthropic.d.ts.map +1 -0
  80. package/dist/aria-connector/src/providers/anthropic.js +92 -0
  81. package/dist/aria-connector/src/providers/anthropic.js.map +1 -0
  82. package/dist/aria-connector/src/providers/deepseek.d.ts +3 -0
  83. package/dist/aria-connector/src/providers/deepseek.d.ts.map +1 -0
  84. package/dist/aria-connector/src/providers/deepseek.js +28 -0
  85. package/dist/aria-connector/src/providers/deepseek.js.map +1 -0
  86. package/dist/aria-connector/src/providers/google.d.ts +3 -0
  87. package/dist/aria-connector/src/providers/google.d.ts.map +1 -0
  88. package/dist/aria-connector/src/providers/google.js +38 -0
  89. package/dist/aria-connector/src/providers/google.js.map +1 -0
  90. package/dist/aria-connector/src/providers/ollama.d.ts +3 -0
  91. package/dist/aria-connector/src/providers/ollama.d.ts.map +1 -0
  92. package/dist/aria-connector/src/providers/ollama.js +28 -0
  93. package/dist/aria-connector/src/providers/ollama.js.map +1 -0
  94. package/dist/aria-connector/src/providers/openai.d.ts +4 -0
  95. package/dist/aria-connector/src/providers/openai.d.ts.map +1 -0
  96. package/dist/aria-connector/src/providers/openai.js +84 -0
  97. package/dist/aria-connector/src/providers/openai.js.map +1 -0
  98. package/dist/aria-connector/src/providers/openrouter.d.ts +3 -0
  99. package/dist/aria-connector/src/providers/openrouter.d.ts.map +1 -0
  100. package/dist/aria-connector/src/providers/openrouter.js +30 -0
  101. package/dist/aria-connector/src/providers/openrouter.js.map +1 -0
  102. package/dist/aria-connector/src/providers/types.d.ts +20 -0
  103. package/dist/aria-connector/src/providers/types.d.ts.map +1 -0
  104. package/dist/aria-connector/src/providers/types.js +2 -0
  105. package/dist/aria-connector/src/providers/types.js.map +1 -0
  106. package/dist/aria-connector/src/setup-wizard.d.ts +2 -0
  107. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -0
  108. package/dist/aria-connector/src/setup-wizard.js +140 -0
  109. package/dist/aria-connector/src/setup-wizard.js.map +1 -0
  110. package/dist/aria-connector/src/types.d.ts +30 -0
  111. package/dist/aria-connector/src/types.d.ts.map +1 -0
  112. package/dist/aria-connector/src/types.js +5 -0
  113. package/dist/aria-connector/src/types.js.map +1 -0
  114. package/dist/aria-web/src/lib/codebase-scanner.d.ts +127 -0
  115. package/dist/aria-web/src/lib/codebase-scanner.d.ts.map +1 -0
  116. package/dist/aria-web/src/lib/codebase-scanner.js +1730 -0
  117. package/dist/aria-web/src/lib/codebase-scanner.js.map +1 -0
  118. package/dist/cli-0.2.0.tgz +0 -0
  119. package/dist/install.sh +13 -0
  120. package/hooks/aria-harness-via-sdk.mjs +317 -0
  121. package/hooks/aria-pre-tool-gate.mjs +596 -0
  122. package/hooks/aria-preprompt-consult.mjs +175 -0
  123. package/hooks/aria-stop-gate.mjs +222 -0
  124. package/package.json +47 -0
  125. package/src/__tests__/auth-commands.test.ts +132 -0
  126. package/src/auth-commands.ts +175 -0
  127. package/src/auth.ts +33 -0
  128. package/src/auto-mcp.ts +1172 -0
  129. package/src/chat.ts +387 -0
  130. package/src/codebase-scanner.ts +18 -0
  131. package/src/cognition-log.ts +30 -0
  132. package/src/config.ts +94 -0
  133. package/src/connectors/claude-code.ts +213 -0
  134. package/src/connectors/cursor.ts +75 -0
  135. package/src/connectors/opencode.ts +115 -0
  136. package/src/connectors/shell.ts +72 -0
  137. package/src/garden-client.ts +98 -0
  138. package/src/garden-control-plane.ts +108 -0
  139. package/src/harness-client.ts +454 -0
  140. package/src/hive-client.ts +104 -0
  141. package/src/index.ts +26 -0
  142. package/src/install-hooks.ts +259 -0
  143. package/src/model-context.ts +88 -0
  144. package/src/persona.ts +113 -0
  145. package/src/providers/anthropic.ts +120 -0
  146. package/src/providers/deepseek.ts +40 -0
  147. package/src/providers/google.ts +57 -0
  148. package/src/providers/ollama.ts +43 -0
  149. package/src/providers/openai.ts +108 -0
  150. package/src/providers/openrouter.ts +42 -0
  151. package/src/providers/types.ts +35 -0
  152. package/src/setup-wizard.ts +177 -0
  153. package/src/types.ts +32 -0
@@ -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
+ }