@veloxts/mcp 0.6.54 → 0.6.55

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @veloxts/mcp
2
2
 
3
+ ## 0.6.55
4
+
5
+ ### Patch Changes
6
+
7
+ - feat(mcp): add static TypeScript analyzer for procedure discovery
8
+ - Updated dependencies
9
+ - @veloxts/cli@0.6.55
10
+ - @veloxts/router@0.6.55
11
+ - @veloxts/validation@0.6.55
12
+
3
13
  ## 0.6.54
4
14
 
5
15
  ### Patch Changes
@@ -2,6 +2,7 @@
2
2
  * Procedures Resource
3
3
  *
4
4
  * Exposes VeloxTS procedure information to AI tools.
5
+ * Uses dynamic discovery when possible, falls back to static analysis for TypeScript files.
5
6
  */
6
7
  /**
7
8
  * Information about a single procedure
@@ -44,6 +45,9 @@ export interface ProceduresResourceResponse {
44
45
  }
45
46
  /**
46
47
  * Discover and return procedure information for a project
48
+ *
49
+ * Uses static TypeScript analysis as primary method (works with uncompiled TS),
50
+ * falls back to dynamic discovery for compiled projects.
47
51
  */
48
52
  export declare function getProcedures(projectRoot: string): Promise<ProceduresResourceResponse>;
49
53
  /**
@@ -2,9 +2,11 @@
2
2
  * Procedures Resource
3
3
  *
4
4
  * Exposes VeloxTS procedure information to AI tools.
5
+ * Uses dynamic discovery when possible, falls back to static analysis for TypeScript files.
5
6
  */
6
7
  import { discoverProceduresVerbose, getRouteSummary } from '@veloxts/router';
7
8
  import { getProceduresPath } from '../utils/project.js';
9
+ import { analyzeDirectory } from './static-analyzer.js';
8
10
  // ============================================================================
9
11
  // Resource Handler
10
12
  // ============================================================================
@@ -48,47 +50,85 @@ function extractProcedureInfo(collections) {
48
50
  }
49
51
  /**
50
52
  * Discover and return procedure information for a project
53
+ *
54
+ * Uses static TypeScript analysis as primary method (works with uncompiled TS),
55
+ * falls back to dynamic discovery for compiled projects.
51
56
  */
52
57
  export async function getProcedures(projectRoot) {
53
58
  const proceduresPath = getProceduresPath(projectRoot);
54
59
  if (!proceduresPath) {
55
- return {
56
- procedures: [],
57
- namespaces: [],
58
- totalCount: 0,
59
- queries: 0,
60
- mutations: 0,
61
- };
60
+ return emptyResponse();
61
+ }
62
+ // Primary: Static TypeScript analysis (works with uncompiled .ts files)
63
+ const staticResult = analyzeDirectory(proceduresPath);
64
+ if (staticResult.procedures.length > 0) {
65
+ return formatStaticResult(staticResult);
62
66
  }
63
- let result;
67
+ // Fallback: Dynamic discovery (works with compiled .js files or ts-node)
64
68
  try {
65
- result = await discoverProceduresVerbose(proceduresPath, {
69
+ const dynamicResult = await discoverProceduresVerbose(proceduresPath, {
66
70
  recursive: true,
67
- onInvalidExport: 'warn',
71
+ onInvalidExport: 'silent',
68
72
  });
69
- }
70
- catch {
73
+ const { procedures, namespaces } = extractProcedureInfo(dynamicResult.collections);
74
+ const queries = procedures.filter((p) => p.type === 'query').length;
75
+ const mutations = procedures.filter((p) => p.type === 'mutation').length;
71
76
  return {
72
- procedures: [],
73
- namespaces: [],
74
- totalCount: 0,
75
- queries: 0,
76
- mutations: 0,
77
+ procedures,
78
+ namespaces,
79
+ totalCount: procedures.length,
80
+ queries,
81
+ mutations,
82
+ discoveryInfo: {
83
+ scannedFiles: dynamicResult.scannedFiles.length,
84
+ loadedFiles: dynamicResult.loadedFiles.length,
85
+ warnings: dynamicResult.warnings.length,
86
+ },
77
87
  };
78
88
  }
79
- const { procedures, namespaces } = extractProcedureInfo(result.collections);
89
+ catch {
90
+ // Dynamic discovery failed (expected for uncompiled TS projects)
91
+ }
92
+ return emptyResponse();
93
+ }
94
+ /**
95
+ * Create empty response
96
+ */
97
+ function emptyResponse() {
98
+ return {
99
+ procedures: [],
100
+ namespaces: [],
101
+ totalCount: 0,
102
+ queries: 0,
103
+ mutations: 0,
104
+ };
105
+ }
106
+ /**
107
+ * Format static analysis result as ProceduresResourceResponse
108
+ */
109
+ function formatStaticResult(staticResult) {
110
+ const procedures = staticResult.procedures.map((p) => ({
111
+ name: p.name,
112
+ namespace: p.namespace,
113
+ type: p.type === 'unknown' ? 'query' : p.type, // Default unknown to query
114
+ hasInputSchema: p.hasInputSchema,
115
+ hasOutputSchema: p.hasOutputSchema,
116
+ guardCount: p.hasGuards ? 1 : 0,
117
+ middlewareCount: p.hasMiddleware ? 1 : 0,
118
+ route: p.route,
119
+ }));
80
120
  const queries = procedures.filter((p) => p.type === 'query').length;
81
121
  const mutations = procedures.filter((p) => p.type === 'mutation').length;
82
122
  return {
83
123
  procedures,
84
- namespaces,
124
+ namespaces: staticResult.namespaces,
85
125
  totalCount: procedures.length,
86
126
  queries,
87
127
  mutations,
88
128
  discoveryInfo: {
89
- scannedFiles: result.scannedFiles.length,
90
- loadedFiles: result.loadedFiles.length,
91
- warnings: result.warnings.length,
129
+ scannedFiles: staticResult.files.length,
130
+ loadedFiles: staticResult.files.length,
131
+ warnings: staticResult.errors.length,
92
132
  },
93
133
  };
94
134
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Static TypeScript Analyzer
3
+ *
4
+ * Extracts procedure information from TypeScript source files without execution.
5
+ * Uses TypeScript Compiler API in parse-only mode for accurate AST analysis.
6
+ * Falls back to regex for edge cases.
7
+ */
8
+ export interface StaticProcedureInfo {
9
+ name: string;
10
+ namespace: string;
11
+ type: 'query' | 'mutation' | 'unknown';
12
+ hasInputSchema: boolean;
13
+ hasOutputSchema: boolean;
14
+ hasGuards: boolean;
15
+ hasMiddleware: boolean;
16
+ route?: {
17
+ method: string;
18
+ path: string;
19
+ };
20
+ restOverride?: {
21
+ method?: string;
22
+ path?: string;
23
+ };
24
+ }
25
+ export interface StaticAnalysisResult {
26
+ procedures: StaticProcedureInfo[];
27
+ namespaces: string[];
28
+ files: string[];
29
+ errors: string[];
30
+ }
31
+ /**
32
+ * Analyze a procedures directory statically
33
+ */
34
+ export declare function analyzeDirectory(proceduresPath: string): StaticAnalysisResult;
35
+ /**
36
+ * Format static analysis result as text
37
+ */
38
+ export declare function formatStaticAnalysisAsText(result: StaticAnalysisResult): string;
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Static TypeScript Analyzer
3
+ *
4
+ * Extracts procedure information from TypeScript source files without execution.
5
+ * Uses TypeScript Compiler API in parse-only mode for accurate AST analysis.
6
+ * Falls back to regex for edge cases.
7
+ */
8
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
9
+ import { basename, extname, join } from 'node:path';
10
+ import ts from 'typescript';
11
+ // ============================================================================
12
+ // Procedure Name to HTTP Method Mapping
13
+ // ============================================================================
14
+ const METHOD_PREFIXES = {
15
+ get: { method: 'GET', type: 'query' },
16
+ list: { method: 'GET', type: 'query' },
17
+ find: { method: 'GET', type: 'query' },
18
+ search: { method: 'GET', type: 'query' },
19
+ create: { method: 'POST', type: 'mutation' },
20
+ add: { method: 'POST', type: 'mutation' },
21
+ update: { method: 'PUT', type: 'mutation' },
22
+ edit: { method: 'PUT', type: 'mutation' },
23
+ patch: { method: 'PATCH', type: 'mutation' },
24
+ delete: { method: 'DELETE', type: 'mutation' },
25
+ remove: { method: 'DELETE', type: 'mutation' },
26
+ };
27
+ // ============================================================================
28
+ // Main Analysis Functions
29
+ // ============================================================================
30
+ /**
31
+ * Analyze a procedures directory statically
32
+ */
33
+ export function analyzeDirectory(proceduresPath) {
34
+ const result = {
35
+ procedures: [],
36
+ namespaces: [],
37
+ files: [],
38
+ errors: [],
39
+ };
40
+ try {
41
+ const entries = readdirSync(proceduresPath);
42
+ for (const entry of entries) {
43
+ const fullPath = join(proceduresPath, entry);
44
+ try {
45
+ const stat = statSync(fullPath);
46
+ if (stat.isFile() && isTypeScriptFile(entry) && !isExcluded(entry)) {
47
+ result.files.push(fullPath);
48
+ try {
49
+ const content = readFileSync(fullPath, 'utf-8');
50
+ const collections = analyzeFileWithAST(fullPath, content);
51
+ for (const collection of collections) {
52
+ if (!result.namespaces.includes(collection.namespace)) {
53
+ result.namespaces.push(collection.namespace);
54
+ }
55
+ for (const proc of collection.procedures) {
56
+ const info = toProcedureInfo(proc, collection.namespace);
57
+ result.procedures.push(info);
58
+ }
59
+ }
60
+ }
61
+ catch (err) {
62
+ result.errors.push(`Error analyzing ${entry}: ${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ }
65
+ }
66
+ catch (err) {
67
+ result.errors.push(`Error accessing ${entry}: ${err instanceof Error ? err.message : String(err)}`);
68
+ }
69
+ }
70
+ }
71
+ catch (err) {
72
+ result.errors.push(`Error reading directory: ${err instanceof Error ? err.message : String(err)}`);
73
+ }
74
+ return result;
75
+ }
76
+ // ============================================================================
77
+ // TypeScript Compiler API Analysis
78
+ // ============================================================================
79
+ /**
80
+ * Parse a TypeScript file and extract procedure information using the TS Compiler API.
81
+ * Uses parse-only mode (no type checking) for speed and to avoid import resolution.
82
+ */
83
+ function analyzeFileWithAST(filePath, content) {
84
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, // setParentNodes - needed for tree traversal
85
+ ts.ScriptKind.TS);
86
+ const collections = [];
87
+ // Visit all nodes looking for procedures() or defineProcedures() calls
88
+ function visit(node) {
89
+ if (ts.isCallExpression(node)) {
90
+ const collection = tryExtractProcedureCollection(node);
91
+ if (collection) {
92
+ collections.push({ ...collection, filePath });
93
+ }
94
+ }
95
+ ts.forEachChild(node, visit);
96
+ }
97
+ visit(sourceFile);
98
+ // If no collections found via AST, try regex fallback
99
+ if (collections.length === 0) {
100
+ const regexResult = analyzeFileWithRegex(filePath, content);
101
+ if (regexResult) {
102
+ collections.push(regexResult);
103
+ }
104
+ }
105
+ return collections;
106
+ }
107
+ /**
108
+ * Try to extract a procedure collection from a call expression.
109
+ * Looks for: procedures('namespace', { ... }) or defineProcedures('namespace', { ... })
110
+ */
111
+ function tryExtractProcedureCollection(node) {
112
+ // Check if this is a procedures() or defineProcedures() call
113
+ const callee = node.expression;
114
+ let functionName;
115
+ if (ts.isIdentifier(callee)) {
116
+ functionName = callee.text;
117
+ }
118
+ if (functionName !== 'procedures' && functionName !== 'defineProcedures') {
119
+ return null;
120
+ }
121
+ // Extract namespace from first argument
122
+ const [namespaceArg, proceduresArg] = node.arguments;
123
+ if (!namespaceArg || !ts.isStringLiteral(namespaceArg)) {
124
+ return null;
125
+ }
126
+ const namespace = namespaceArg.text;
127
+ // Extract procedures from second argument (object literal)
128
+ if (!proceduresArg || !ts.isObjectLiteralExpression(proceduresArg)) {
129
+ return null;
130
+ }
131
+ const procedures = [];
132
+ for (const prop of proceduresArg.properties) {
133
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
134
+ const procedureName = prop.name.text;
135
+ const procedureInfo = extractProcedureInfo(prop.initializer);
136
+ procedures.push({
137
+ name: procedureName,
138
+ ...procedureInfo,
139
+ });
140
+ }
141
+ }
142
+ return { namespace, procedures };
143
+ }
144
+ /**
145
+ * Extract procedure information from a procedure builder chain.
146
+ * Handles: procedure().input(...).output(...).guard(...).query/mutation(...)
147
+ */
148
+ function extractProcedureInfo(node) {
149
+ const info = {
150
+ type: 'unknown',
151
+ hasInput: false,
152
+ hasOutput: false,
153
+ hasGuard: false,
154
+ hasMiddleware: false,
155
+ };
156
+ // Walk the call chain
157
+ function walkChain(n) {
158
+ if (!ts.isCallExpression(n))
159
+ return;
160
+ const callee = n.expression;
161
+ // Check for method calls on the chain
162
+ if (ts.isPropertyAccessExpression(callee)) {
163
+ const methodName = callee.name.text;
164
+ switch (methodName) {
165
+ case 'query':
166
+ info.type = 'query';
167
+ break;
168
+ case 'mutation':
169
+ info.type = 'mutation';
170
+ break;
171
+ case 'input':
172
+ info.hasInput = true;
173
+ break;
174
+ case 'output':
175
+ info.hasOutput = true;
176
+ break;
177
+ case 'guard':
178
+ info.hasGuard = true;
179
+ break;
180
+ case 'use':
181
+ info.hasMiddleware = true;
182
+ break;
183
+ case 'rest':
184
+ info.restOverride = extractRestOverride(n.arguments[0]);
185
+ break;
186
+ }
187
+ // Continue walking up the chain
188
+ walkChain(callee.expression);
189
+ }
190
+ }
191
+ walkChain(node);
192
+ return info;
193
+ }
194
+ /**
195
+ * Extract REST override configuration from .rest({ method, path }) call
196
+ */
197
+ function extractRestOverride(arg) {
198
+ if (!arg || !ts.isObjectLiteralExpression(arg)) {
199
+ return undefined;
200
+ }
201
+ const result = {};
202
+ for (const prop of arg.properties) {
203
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
204
+ const key = prop.name.text;
205
+ if ((key === 'method' || key === 'path') && ts.isStringLiteral(prop.initializer)) {
206
+ result[key] = prop.initializer.text;
207
+ }
208
+ }
209
+ }
210
+ return Object.keys(result).length > 0 ? result : undefined;
211
+ }
212
+ // ============================================================================
213
+ // Regex Fallback Analysis
214
+ // ============================================================================
215
+ /**
216
+ * Fallback regex-based analysis for files that don't match the standard pattern
217
+ */
218
+ function analyzeFileWithRegex(filePath, content) {
219
+ const procedures = [];
220
+ let namespace = '';
221
+ // Extract namespace from procedures() call
222
+ const namespaceMatch = content.match(/procedures\s*\(\s*['"]([^'"]+)['"]/);
223
+ if (namespaceMatch) {
224
+ namespace = namespaceMatch[1];
225
+ }
226
+ else {
227
+ // Derive from filename
228
+ const filename = basename(filePath, extname(filePath));
229
+ if (filename !== 'index') {
230
+ namespace = filename;
231
+ }
232
+ }
233
+ if (!namespace) {
234
+ return null;
235
+ }
236
+ // Extract procedure names using regex
237
+ const procedurePattern = /(\w+)\s*:\s*procedure\s*[.(]/g;
238
+ const matches = content.matchAll(procedurePattern);
239
+ for (const match of matches) {
240
+ const name = match[1];
241
+ // Determine type from naming convention
242
+ let type = 'unknown';
243
+ for (const [prefix, info] of Object.entries(METHOD_PREFIXES)) {
244
+ if (name.toLowerCase().startsWith(prefix)) {
245
+ type = info.type;
246
+ break;
247
+ }
248
+ }
249
+ // Check for explicit .query() or .mutation()
250
+ if (content.includes(`${name}`) && content.includes('.query(')) {
251
+ type = 'query';
252
+ }
253
+ else if (content.includes(`${name}`) && content.includes('.mutation(')) {
254
+ type = 'mutation';
255
+ }
256
+ procedures.push({
257
+ name,
258
+ type,
259
+ hasInput: content.includes('.input('),
260
+ hasOutput: content.includes('.output('),
261
+ hasGuard: content.includes('.guard('),
262
+ hasMiddleware: content.includes('.use('),
263
+ });
264
+ }
265
+ if (procedures.length === 0) {
266
+ return null;
267
+ }
268
+ return { namespace, procedures, filePath };
269
+ }
270
+ // ============================================================================
271
+ // Helpers
272
+ // ============================================================================
273
+ /**
274
+ * Convert parsed procedure to StaticProcedureInfo
275
+ */
276
+ function toProcedureInfo(proc, namespace) {
277
+ const route = inferRestRoute(proc.name, namespace, proc.restOverride);
278
+ return {
279
+ name: proc.name,
280
+ namespace,
281
+ type: proc.type,
282
+ hasInputSchema: proc.hasInput,
283
+ hasOutputSchema: proc.hasOutput,
284
+ hasGuards: proc.hasGuard,
285
+ hasMiddleware: proc.hasMiddleware,
286
+ route,
287
+ restOverride: proc.restOverride,
288
+ };
289
+ }
290
+ /**
291
+ * Infer REST route from procedure name and namespace
292
+ */
293
+ function inferRestRoute(procedureName, namespace, override) {
294
+ const basePath = `/api/${namespace}`;
295
+ // Use override if provided
296
+ if (override?.method && override?.path) {
297
+ return { method: override.method, path: override.path };
298
+ }
299
+ // Infer from naming convention
300
+ for (const [prefix, info] of Object.entries(METHOD_PREFIXES)) {
301
+ if (procedureName.toLowerCase().startsWith(prefix)) {
302
+ // Collection endpoints (no :id)
303
+ if (['list', 'find', 'search', 'create', 'add'].includes(prefix)) {
304
+ return {
305
+ method: override?.method || info.method,
306
+ path: override?.path || basePath,
307
+ };
308
+ }
309
+ // Resource endpoints (with :id)
310
+ return {
311
+ method: override?.method || info.method,
312
+ path: override?.path || `${basePath}/:id`,
313
+ };
314
+ }
315
+ }
316
+ // Default to GET collection
317
+ return { method: 'GET', path: basePath };
318
+ }
319
+ /**
320
+ * Check if file is a TypeScript file
321
+ */
322
+ function isTypeScriptFile(filename) {
323
+ const ext = extname(filename);
324
+ return ['.ts', '.tsx', '.mts'].includes(ext);
325
+ }
326
+ /**
327
+ * Check if file should be excluded
328
+ */
329
+ function isExcluded(filename) {
330
+ return (filename.startsWith('_') ||
331
+ filename.endsWith('.test.ts') ||
332
+ filename.endsWith('.spec.ts') ||
333
+ filename.endsWith('.d.ts') ||
334
+ filename === 'index.ts');
335
+ }
336
+ // ============================================================================
337
+ // Formatting
338
+ // ============================================================================
339
+ /**
340
+ * Format static analysis result as text
341
+ */
342
+ export function formatStaticAnalysisAsText(result) {
343
+ const lines = [
344
+ '# VeloxTS Procedures',
345
+ '',
346
+ `Total: ${result.procedures.length} procedures`,
347
+ `Namespaces: ${result.namespaces.join(', ') || 'none'}`,
348
+ `Files analyzed: ${result.files.length}`,
349
+ '',
350
+ ];
351
+ if (result.errors.length > 0) {
352
+ lines.push('## Analysis Notes');
353
+ for (const error of result.errors) {
354
+ lines.push(`- ${error}`);
355
+ }
356
+ lines.push('');
357
+ }
358
+ // Group by namespace
359
+ const byNamespace = new Map();
360
+ for (const proc of result.procedures) {
361
+ const list = byNamespace.get(proc.namespace) ?? [];
362
+ list.push(proc);
363
+ byNamespace.set(proc.namespace, list);
364
+ }
365
+ for (const [namespace, procs] of byNamespace) {
366
+ lines.push(`## ${namespace}`);
367
+ lines.push('');
368
+ for (const proc of procs) {
369
+ const type = proc.type === 'query' ? 'Q' : proc.type === 'mutation' ? 'M' : '?';
370
+ const route = proc.route ? ` -> ${proc.route.method} ${proc.route.path}` : '';
371
+ const guards = proc.hasGuards ? ' [guarded]' : '';
372
+ lines.push(`- [${type}] ${proc.name}${route}${guards}`);
373
+ }
374
+ lines.push('');
375
+ }
376
+ return lines.join('\n');
377
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veloxts/mcp",
3
- "version": "0.6.54",
3
+ "version": "0.6.55",
4
4
  "description": "Model Context Protocol server for VeloxTS - expose project context to AI tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,9 +23,10 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/sdk": "1.25.1",
26
- "@veloxts/cli": "0.6.54",
27
- "@veloxts/router": "0.6.54",
28
- "@veloxts/validation": "0.6.54"
26
+ "typescript": "5.9.3",
27
+ "@veloxts/cli": "0.6.55",
28
+ "@veloxts/router": "0.6.55",
29
+ "@veloxts/validation": "0.6.55"
29
30
  },
30
31
  "peerDependencies": {
31
32
  "zod": ">=3.25.0"