@veloxts/mcp 0.6.54 → 0.6.56

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