@tng-sh/mcp-server 1.0.3 → 1.0.5

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 (50) hide show
  1. package/dist/index.js +283 -88
  2. package/dist/index.js.map +1 -1
  3. package/dist/init.d.ts.map +1 -1
  4. package/dist/init.js +8 -0
  5. package/dist/init.js.map +1 -1
  6. package/dist/prompts.d.ts +3 -0
  7. package/dist/prompts.d.ts.map +1 -0
  8. package/dist/prompts.js +63 -0
  9. package/dist/prompts.js.map +1 -0
  10. package/dist/tools/audit.d.ts +5 -2
  11. package/dist/tools/audit.d.ts.map +1 -1
  12. package/dist/tools/audit.js +115 -147
  13. package/dist/tools/audit.js.map +1 -1
  14. package/dist/tools/call_sites.d.ts +4 -1
  15. package/dist/tools/call_sites.d.ts.map +1 -1
  16. package/dist/tools/call_sites.js +16 -23
  17. package/dist/tools/call_sites.js.map +1 -1
  18. package/dist/tools/clones.d.ts +4 -1
  19. package/dist/tools/clones.d.ts.map +1 -1
  20. package/dist/tools/clones.js +19 -24
  21. package/dist/tools/clones.js.map +1 -1
  22. package/dist/tools/deadcode.d.ts +4 -1
  23. package/dist/tools/deadcode.d.ts.map +1 -1
  24. package/dist/tools/deadcode.js +19 -24
  25. package/dist/tools/deadcode.js.map +1 -1
  26. package/dist/tools/generate.d.ts +4 -1
  27. package/dist/tools/generate.d.ts.map +1 -1
  28. package/dist/tools/generate.js +19 -24
  29. package/dist/tools/generate.js.map +1 -1
  30. package/dist/tools/impact.d.ts +4 -1
  31. package/dist/tools/impact.d.ts.map +1 -1
  32. package/dist/tools/impact.js +19 -24
  33. package/dist/tools/impact.js.map +1 -1
  34. package/dist/tools/method_resolver.d.ts +20 -0
  35. package/dist/tools/method_resolver.d.ts.map +1 -0
  36. package/dist/tools/method_resolver.js +138 -0
  37. package/dist/tools/method_resolver.js.map +1 -0
  38. package/dist/tools/trace.d.ts +4 -1
  39. package/dist/tools/trace.d.ts.map +1 -1
  40. package/dist/tools/trace.js +19 -24
  41. package/dist/tools/trace.js.map +1 -1
  42. package/dist/tools/utils.d.ts +36 -1
  43. package/dist/tools/utils.d.ts.map +1 -1
  44. package/dist/tools/utils.js +305 -17
  45. package/dist/tools/utils.js.map +1 -1
  46. package/dist/tools/xray.d.ts +4 -1
  47. package/dist/tools/xray.d.ts.map +1 -1
  48. package/dist/tools/xray.js +21 -23
  49. package/dist/tools/xray.js.map +1 -1
  50. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11,31 +11,39 @@ import { detectDeadCode } from './tools/deadcode.js';
11
11
  import { findCallSites } from './tools/call_sites.js';
12
12
  import { impactMethod } from './tools/impact.js';
13
13
  import { runInit } from './init.js';
14
- import { existsSync, readFileSync } from 'fs';
15
- import { resolveFilePath, findProjectRoot } from './tools/utils.js';
14
+ import { preflightTarget, buildMcpResponse } from './tools/utils.js';
15
+ import { registerPrompts } from './prompts.js';
16
16
  const server = new McpServer({
17
17
  name: 'tng-mcp',
18
- version: '1.0.0',
18
+ version: '1.1.0',
19
19
  });
20
20
  if (process.argv.includes('init')) {
21
21
  const code = runInit();
22
22
  process.exit(code);
23
23
  }
24
24
  const auditSchema = {
25
- file_path: z.string().describe('Relative or absolute path to the source file (e.g., "app/services/payment_processor.rb", "src/utils/validator.ts")'),
25
+ file_path: z.string().optional().describe('Relative or absolute path to the source file (optional when method name is unique in project)'),
26
26
  method_name: z.string().describe('Name of the method/function to audit (e.g., "process_payment", "validateUser", "handleSubmit")'),
27
27
  class_name: z.string().optional().describe('Class or module name to disambiguate methods with the same name'),
28
- test_type: z.string().optional().describe('Component type - REQUIRED for JavaScript/Python: "utility", "react_component", "express_handler", "django_view", etc. Ignored for Ruby/Rails.'),
28
+ test_type: z.string().optional().describe('Component type - REQUIRED for JavaScript/Python/Ruby. You MUST auto-detect this based on the code. For JS/TS choose from: react_component, express_handler, nest_js, orm_model, graphql_resolver, background_job, mailer, utility. For Python choose from: endpoint, service, model, job, mailer, utility. For Ruby choose from: adapter, cli, generator, http_endpoint, job, lib, mailer, model, serializer, service, utility. Ignored for Rails.'),
29
29
  response_format: z.string().optional().describe('Response format: "text" (default), "json", or "both".'),
30
30
  raw_json: z.boolean().optional().describe('If true, return JSON envelope with raw tool output.'),
31
+ project_root: z.string().optional().describe('Project root for deterministic relative-path resolution.'),
32
+ timeout_sec: z.number().int().positive().optional().describe('Execution timeout in seconds (default 420).'),
33
+ retries: z.number().int().min(0).max(3).optional().describe('Retry attempts for transient failures (default 0).'),
34
+ debug: z.boolean().optional().describe('Return additional diagnostics in server logs/metadata.'),
31
35
  };
32
36
  const generateSchema = {
33
37
  file_path: z.string().describe('Relative or absolute path to the source file to generate tests for'),
34
38
  method_name: z.string().describe('Name of the method/function to generate tests for'),
35
39
  class_name: z.string().optional().describe('Class or module name to disambiguate methods with the same name'),
36
- test_type: z.string().optional().describe('Component type or test framework hint - REQUIRED for JS/Python: "utility", "react_component", "express_handler", etc.'),
40
+ test_type: z.string().optional().describe('Component type - REQUIRED for JavaScript/Python/Ruby. You MUST auto-detect this based on the code. For JS/TS choose from: react_component, express_handler, nest_js, orm_model, graphql_resolver, background_job, mailer, utility. For Python choose from: endpoint, service, model, job, mailer, utility. For Ruby choose from: adapter, cli, generator, http_endpoint, job, lib, mailer, model, serializer, service, utility. Ignored for Rails.'),
37
41
  response_format: z.string().optional().describe('Response format: "text" (default), "json", or "both".'),
38
42
  raw_json: z.boolean().optional().describe('If true, return JSON envelope with raw tool output.'),
43
+ project_root: z.string().optional().describe('Project root for deterministic relative-path resolution.'),
44
+ timeout_sec: z.number().int().positive().optional().describe('Execution timeout in seconds (default 420).'),
45
+ retries: z.number().int().min(0).max(3).optional().describe('Retry attempts for transient failures (default 0).'),
46
+ debug: z.boolean().optional().describe('Return additional diagnostics in server logs/metadata.'),
39
47
  };
40
48
  const methodSchema = {
41
49
  file_path: z.string().describe('Relative or absolute path to the source file'),
@@ -43,6 +51,10 @@ const methodSchema = {
43
51
  class_name: z.string().optional().describe('Class or module name to disambiguate methods with the same name'),
44
52
  response_format: z.string().optional().describe('Response format: "text" (default), "json", or "both".'),
45
53
  raw_json: z.boolean().optional().describe('If true, return JSON envelope with raw tool output.'),
54
+ project_root: z.string().optional().describe('Project root for deterministic relative-path resolution.'),
55
+ timeout_sec: z.number().int().positive().optional().describe('Execution timeout in seconds (default 120 for trace/xray).'),
56
+ retries: z.number().int().min(0).max(3).optional().describe('Retry attempts for transient failures (default 0).'),
57
+ debug: z.boolean().optional().describe('Return additional diagnostics in server logs/metadata.'),
46
58
  };
47
59
  const impactSchema = {
48
60
  file_path: z.string().describe('Relative or absolute path to the source file'),
@@ -50,12 +62,20 @@ const impactSchema = {
50
62
  class_name: z.string().optional().describe('Class or module name to disambiguate methods with the same name'),
51
63
  response_format: z.string().optional().describe('Response format: "text" (default), "json", or "both".'),
52
64
  raw_json: z.boolean().optional().describe('If true, return JSON envelope with raw tool output.'),
65
+ project_root: z.string().optional().describe('Project root for deterministic relative-path resolution.'),
66
+ timeout_sec: z.number().int().positive().optional().describe('Execution timeout in seconds (default 180).'),
67
+ retries: z.number().int().min(0).max(3).optional().describe('Retry attempts for transient failures (default 0).'),
68
+ debug: z.boolean().optional().describe('Return additional diagnostics in server logs/metadata.'),
53
69
  };
54
70
  const clonesSchema = {
55
71
  file_path: z.string().describe('Relative or absolute path to the source file to analyze for duplication'),
56
72
  level: z.string().optional().describe('Clone detection level: "1" (exact), "2" (structural), "3" (fuzzy/near-miss), or "all" (default - detects all types)'),
57
73
  response_format: z.string().optional().describe('Response format: "text" (default), "json", or "both".'),
58
74
  raw_json: z.boolean().optional().describe('If true, return JSON envelope with raw tool output.'),
75
+ project_root: z.string().optional().describe('Project root for deterministic relative-path resolution.'),
76
+ timeout_sec: z.number().int().positive().optional().describe('Execution timeout in seconds (default 180).'),
77
+ retries: z.number().int().min(0).max(3).optional().describe('Retry attempts for transient failures (default 0).'),
78
+ debug: z.boolean().optional().describe('Return additional diagnostics in server logs/metadata.'),
59
79
  };
60
80
  const deadCodeSchema = {
61
81
  file_path: z.string().optional().describe('Relative or absolute path to the source file to analyze for dead code (omit when using all=true)'),
@@ -63,6 +83,9 @@ const deadCodeSchema = {
63
83
  all: z.boolean().optional().describe('If true, analyze dead code across the entire project (passes --all).'),
64
84
  response_format: z.string().optional().describe('Response format: "text" (default), "json", or "both".'),
65
85
  raw_json: z.boolean().optional().describe('If true, return JSON envelope with raw tool output.'),
86
+ timeout_sec: z.number().int().positive().optional().describe('Execution timeout in seconds (default 600).'),
87
+ retries: z.number().int().min(0).max(3).optional().describe('Retry attempts for transient failures (default 0).'),
88
+ debug: z.boolean().optional().describe('Return additional diagnostics in server logs/metadata.'),
66
89
  };
67
90
  const callSitesSchema = {
68
91
  file_path: z.string().describe('Relative or absolute path to the source file containing the method'),
@@ -71,16 +94,22 @@ const callSitesSchema = {
71
94
  project_root: z.string().optional().describe('Project root to resolve relative paths'),
72
95
  response_format: z.string().optional().describe('Response format: "text" (default), "json", or "both".'),
73
96
  raw_json: z.boolean().optional().describe('If true, return JSON envelope with raw tool output.'),
97
+ timeout_sec: z.number().int().positive().optional().describe('Execution timeout in seconds (default 600).'),
98
+ retries: z.number().int().min(0).max(3).optional().describe('Retry attempts for transient failures (default 0).'),
99
+ debug: z.boolean().optional().describe('Return additional diagnostics in server logs/metadata.'),
74
100
  };
75
101
  const auditDescription = `Audit a specific method/function for security, bugs, performance, and code quality issues.
76
102
  Use when the user asks to audit, review, analyze, inspect, or check code.
77
103
  Returns findings with severity, line locations, and suggested fixes.
104
+ IMPORTANT POST-AUDIT INSTRUCTION: Once you have presented the audit findings to the user, you MUST ask them if they would like you to implement the suggested fixes. Do not implement the fixes immediately without asking first.
78
105
  Supports: Ruby, Python, JavaScript/TypeScript.
79
- Example: audit_method { file_path: "app/services/payment_processor.rb", method_name: "process_payment" }`;
106
+ IMPORTANT: For JavaScript/Python/Ruby, the "test_type" argument is MANDATORY. You MUST automatically infer the test_type from the code (e.g., express_handler, react_component, endpoint, utility) and provide it. Do NOT ask the user unless completely ambiguous.
107
+ Example: audit_method { file_path: "src/api/payment.ts", method_name: "processPayment", test_type: "express_handler" }`;
80
108
  const generateDescription = `Generate tests for a specific method/function.
81
109
  Use when the user asks to write tests, create specs, or add coverage.
82
110
  Returns complete test code with suggested file path and command to run.
83
111
  Supports: Ruby, Python, JavaScript/TypeScript.
112
+ IMPORTANT: For JavaScript/Python/Ruby, the "test_type" argument is MANDATORY. You MUST automatically infer the test_type from the code (e.g., express_handler, react_component, endpoint, utility) and provide it. Do NOT ask the user unless completely ambiguous.
84
113
  Example: generate_test { file_path: "src/utils/validator.ts", method_name: "validateUser", test_type: "utility" }`;
85
114
  const xrayDescription = `Generate a Mermaid flowchart for a method/function (X-Ray).
86
115
  Use when the user asks for a diagram, flowchart, or visual explanation.
@@ -107,44 +136,94 @@ Example (project-wide): detect_deadcode { all: true }`;
107
136
  const callSitesDescription = `Find in-repo call sites for a method/function.
108
137
  Use when the user asks "who calls this?" or "where is this used?".
109
138
  Example: find_call_sites { file_path: "src/utils/auth.ts", method_name: "validateUser" }`;
139
+ async function callWithPreflight(toolName, args, handler, options = { requireMethod: false }) {
140
+ const filePath = args?.file_path ? String(args.file_path) : '';
141
+ const canSkipPathPreflight = !!options.allowMissingFilePath
142
+ && filePath.trim().length === 0
143
+ && (toolName === 'audit_method' || !!args?.all);
144
+ if (!canSkipPathPreflight) {
145
+ const preflight = preflightTarget({
146
+ tool: toolName,
147
+ filePath,
148
+ methodName: args?.method_name ? String(args.method_name) : undefined,
149
+ className: args?.class_name ? String(args.class_name) : undefined,
150
+ projectRoot: args?.project_root ? String(args.project_root) : undefined,
151
+ requireMethod: options.requireMethod,
152
+ });
153
+ if (!preflight.ok) {
154
+ const format = args?.response_format || (args?.raw_json ? 'json' : 'text');
155
+ return buildMcpResponse({
156
+ tool: toolName,
157
+ text: preflight.message,
158
+ isError: true,
159
+ format,
160
+ code: preflight.code,
161
+ details: preflight.details,
162
+ meta: { project_root: preflight.projectRoot },
163
+ });
164
+ }
165
+ args = {
166
+ ...args,
167
+ project_root: preflight.projectRoot,
168
+ file_path: preflight.resolvedPath || filePath,
169
+ };
170
+ }
171
+ else {
172
+ args = {
173
+ ...args,
174
+ project_root: args?.project_root || process.cwd(),
175
+ };
176
+ if (options.requireMethod && !args?.method_name) {
177
+ const format = args?.response_format || (args?.raw_json ? 'json' : 'text');
178
+ return buildMcpResponse({
179
+ tool: toolName,
180
+ text: 'method_name is required',
181
+ isError: true,
182
+ format,
183
+ code: 'missing_method_name',
184
+ });
185
+ }
186
+ }
187
+ return handler(args);
188
+ }
110
189
  // 1. Audit Method - AI-Powered Code Analysis
111
- server.registerTool('audit_method', { description: auditDescription, inputSchema: auditSchema }, async (args) => await auditMethod(args));
190
+ server.registerTool('audit_method', { description: auditDescription, inputSchema: auditSchema }, async (args) => await callWithPreflight('audit_method', args, auditMethod, { requireMethod: true, allowMissingFilePath: true }));
112
191
  // Aliases for audit_method
113
- server.registerTool('review_method', { description: 'Alias of audit_method. Use for "review this method" or "code review".', inputSchema: auditSchema }, async (args) => await auditMethod(args));
114
- server.registerTool('analyze_method', { description: 'Alias of audit_method. Use for "analyze this method".', inputSchema: auditSchema }, async (args) => await auditMethod(args));
192
+ server.registerTool('review_method', { description: 'Alias of audit_method. Use for "review this method" or "code review".', inputSchema: auditSchema }, async (args) => await callWithPreflight('audit_method', args, auditMethod, { requireMethod: true, allowMissingFilePath: true }));
193
+ server.registerTool('analyze_method', { description: 'Alias of audit_method. Use for "analyze this method".', inputSchema: auditSchema }, async (args) => await callWithPreflight('audit_method', args, auditMethod, { requireMethod: true, allowMissingFilePath: true }));
115
194
  // 2. Generate Test - AI Test Generation
116
- server.registerTool('generate_test', { description: generateDescription, inputSchema: generateSchema }, async (args) => await generateTest(args));
195
+ server.registerTool('generate_test', { description: generateDescription, inputSchema: generateSchema }, async (args) => await callWithPreflight('generate_test', args, generateTest, { requireMethod: true }));
117
196
  // Aliases for generate_test
118
- server.registerTool('generate_tests', { description: 'Alias of generate_test. Use for "generate tests".', inputSchema: generateSchema }, async (args) => await generateTest(args));
119
- server.registerTool('write_tests', { description: 'Alias of generate_test. Use for "write tests".', inputSchema: generateSchema }, async (args) => await generateTest(args));
197
+ server.registerTool('generate_tests', { description: 'Alias of generate_test. Use for "generate tests".', inputSchema: generateSchema }, async (args) => await callWithPreflight('generate_test', args, generateTest, { requireMethod: true }));
198
+ server.registerTool('write_tests', { description: 'Alias of generate_test. Use for "write tests".', inputSchema: generateSchema }, async (args) => await callWithPreflight('generate_test', args, generateTest, { requireMethod: true }));
120
199
  // 3. X-Ray Visualization - Code Flow Diagrams
121
- server.registerTool('xray_method', { description: xrayDescription, inputSchema: methodSchema }, async (args) => await xrayMethod(args));
200
+ server.registerTool('xray_method', { description: xrayDescription, inputSchema: methodSchema }, async (args) => await callWithPreflight('xray_method', args, xrayMethod, { requireMethod: true }));
122
201
  // Aliases for xray_method
123
- server.registerTool('xray_diagram', { description: 'Alias of xray_method. Use for "diagram" or "flowchart".', inputSchema: methodSchema }, async (args) => await xrayMethod(args));
124
- server.registerTool('visualize_flow', { description: 'Alias of xray_method. Use for "visualize flow".', inputSchema: methodSchema }, async (args) => await xrayMethod(args));
202
+ server.registerTool('xray_diagram', { description: 'Alias of xray_method. Use for "diagram" or "flowchart".', inputSchema: methodSchema }, async (args) => await callWithPreflight('xray_method', args, xrayMethod, { requireMethod: true }));
203
+ server.registerTool('visualize_flow', { description: 'Alias of xray_method. Use for "visualize flow".', inputSchema: methodSchema }, async (args) => await callWithPreflight('xray_method', args, xrayMethod, { requireMethod: true }));
125
204
  // 4. Trace Method - Symbolic Execution Analysis
126
- server.registerTool('trace_method', { description: traceDescription, inputSchema: methodSchema }, async (args) => await traceMethod(args));
205
+ server.registerTool('trace_method', { description: traceDescription, inputSchema: methodSchema }, async (args) => await callWithPreflight('trace_method', args, traceMethod, { requireMethod: true }));
127
206
  // 4.5 Impact Method - Blast Radius Analysis
128
- server.registerTool('impact_method', { description: impactDescription, inputSchema: impactSchema }, async (args) => await impactMethod(args));
207
+ server.registerTool('impact_method', { description: impactDescription, inputSchema: impactSchema }, async (args) => await callWithPreflight('impact_method', args, impactMethod, { requireMethod: true }));
129
208
  // Aliases for impact_method
130
- server.registerTool('impact_analysis', { description: 'Alias of impact_method. Use for "impact analysis" or "blast radius".', inputSchema: impactSchema }, async (args) => await impactMethod(args));
131
- server.registerTool('impact_audit', { description: 'Alias of impact_method. Use for "regression check".', inputSchema: impactSchema }, async (args) => await impactMethod(args));
132
- server.registerTool('regression_check', { description: 'Alias of impact_method. Use for "regression check".', inputSchema: impactSchema }, async (args) => await impactMethod(args));
133
- server.registerTool('blast_radius', { description: 'Alias of impact_method. Use for "blast radius check".', inputSchema: impactSchema }, async (args) => await impactMethod(args));
209
+ server.registerTool('impact_analysis', { description: 'Alias of impact_method. Use for "impact analysis" or "blast radius".', inputSchema: impactSchema }, async (args) => await callWithPreflight('impact_method', args, impactMethod, { requireMethod: true }));
210
+ server.registerTool('impact_audit', { description: 'Alias of impact_method. Use for "regression check".', inputSchema: impactSchema }, async (args) => await callWithPreflight('impact_method', args, impactMethod, { requireMethod: true }));
211
+ server.registerTool('regression_check', { description: 'Alias of impact_method. Use for "regression check".', inputSchema: impactSchema }, async (args) => await callWithPreflight('impact_method', args, impactMethod, { requireMethod: true }));
212
+ server.registerTool('blast_radius', { description: 'Alias of impact_method. Use for "blast radius check".', inputSchema: impactSchema }, async (args) => await callWithPreflight('impact_method', args, impactMethod, { requireMethod: true }));
134
213
  // Aliases for trace_method
135
- server.registerTool('trace_execution', { description: 'Alias of trace_method. Use for "trace execution".', inputSchema: methodSchema }, async (args) => await traceMethod(args));
214
+ server.registerTool('trace_execution', { description: 'Alias of trace_method. Use for "trace execution".', inputSchema: methodSchema }, async (args) => await callWithPreflight('trace_method', args, traceMethod, { requireMethod: true }));
136
215
  // 5. Detect Clones - Code Duplication Detection
137
- server.registerTool('detect_clones', { description: clonesDescription, inputSchema: clonesSchema }, async (args) => await detectClones(args));
216
+ server.registerTool('detect_clones', { description: clonesDescription, inputSchema: clonesSchema }, async (args) => await callWithPreflight('detect_clones', args, detectClones, { requireMethod: false }));
138
217
  // Aliases for detect_clones
139
- server.registerTool('find_clones', { description: 'Alias of detect_clones. Use for "find duplicates".', inputSchema: clonesSchema }, async (args) => await detectClones(args));
218
+ server.registerTool('find_clones', { description: 'Alias of detect_clones. Use for "find duplicates".', inputSchema: clonesSchema }, async (args) => await callWithPreflight('detect_clones', args, detectClones, { requireMethod: false }));
140
219
  // 6. Detect Dead Code - Unused Code Detection
141
- server.registerTool('detect_deadcode', { description: deadcodeDescription, inputSchema: deadCodeSchema }, async (args) => await detectDeadCode(args));
220
+ server.registerTool('detect_deadcode', { description: deadcodeDescription, inputSchema: deadCodeSchema }, async (args) => await callWithPreflight('detect_deadcode', args, detectDeadCode, { requireMethod: false, allowMissingFilePath: true }));
142
221
  // Aliases for detect_deadcode
143
- server.registerTool('find_deadcode', { description: 'Alias of detect_deadcode. Use for "find unused code".', inputSchema: deadCodeSchema }, async (args) => await detectDeadCode(args));
222
+ server.registerTool('find_deadcode', { description: 'Alias of detect_deadcode. Use for "find unused code".', inputSchema: deadCodeSchema }, async (args) => await callWithPreflight('detect_deadcode', args, detectDeadCode, { requireMethod: false, allowMissingFilePath: true }));
144
223
  // 7. Find Call Sites - Usage Detection
145
- server.registerTool('find_call_sites', { description: callSitesDescription, inputSchema: callSitesSchema }, async (args) => await findCallSites(args));
224
+ server.registerTool('find_call_sites', { description: callSitesDescription, inputSchema: callSitesSchema }, async (args) => await callWithPreflight('find_call_sites', args, findCallSites, { requireMethod: true }));
146
225
  // Alias for find_call_sites
147
- server.registerTool('call_sites', { description: 'Alias of find_call_sites. Use for "find usages" or "where is this called".', inputSchema: callSitesSchema }, async (args) => await findCallSites(args));
226
+ server.registerTool('call_sites', { description: 'Alias of find_call_sites. Use for "find usages" or "where is this called".', inputSchema: callSitesSchema }, async (args) => await callWithPreflight('find_call_sites', args, findCallSites, { requireMethod: true }));
148
227
  // 8. Capabilities - List Available Tools
149
228
  server.registerTool('list_capabilities', {
150
229
  description: 'List available TNG MCP tools and when to use them.',
@@ -167,9 +246,14 @@ server.registerTool('list_capabilities', {
167
246
  'Best practices:',
168
247
  '- Prefer file paths relative to the project root.',
169
248
  '- If the server runs outside the repo, pass project_root or use absolute file paths.',
170
- '- For JS/Python, include test_type when generating tests or auditing.',
249
+ '- For JS/Python/Ruby, test_type is MANDATORY when generating tests or auditing. MUST auto-detect from the code.',
250
+ '- All execution tools support timeout_sec, retries, and debug.',
251
+ '- Preflight validates file/method and ignore rules before execution.',
171
252
  '',
172
253
  'Aliases: review_method, analyze_method, generate_tests, write_tests, xray_diagram, visualize_flow, trace_execution, impact_analysis, impact_audit, regression_check, blast_radius, find_clones, find_deadcode.',
254
+ '',
255
+ `Server version: 1.1.0`,
256
+ 'Defaults: timeout (audit/generate)=420s, xray/trace=120s, impact/clones=180s, deadcode/callsites=600s; retries=0.',
173
257
  ].join('\n'),
174
258
  },
175
259
  ],
@@ -186,46 +270,20 @@ server.registerTool('validate_request', {
186
270
  const filePath = String(args?.file_path || '');
187
271
  const methodName = args?.method_name ? String(args.method_name) : '';
188
272
  const projectRoot = args?.project_root ? String(args.project_root) : undefined;
189
- const absoluteFilePath = resolveFilePath(filePath, projectRoot);
190
- const exists = existsSync(absoluteFilePath);
191
- let methodFound = false;
192
- let methodCheckSkipped = false;
193
- let methodCheckError = null;
194
- if (exists && methodName) {
195
- try {
196
- const content = readFileSync(absoluteFilePath, 'utf8');
197
- methodFound = content.includes(methodName);
198
- }
199
- catch (e) {
200
- methodCheckError = e?.message || 'Unknown error reading file';
201
- }
202
- }
203
- else if (!methodName) {
204
- methodCheckSkipped = true;
205
- }
206
- const projectRootDetected = exists ? (projectRoot || findProjectRoot(absoluteFilePath) || null) : null;
273
+ const preflight = preflightTarget({
274
+ tool: 'validate_request',
275
+ filePath,
276
+ methodName: methodName || undefined,
277
+ projectRoot,
278
+ requireMethod: false,
279
+ });
207
280
  const response = {
208
- ok: exists && (methodName ? methodFound : true) && !methodCheckError,
209
- file: {
210
- provided: filePath,
211
- resolved: absoluteFilePath,
212
- exists,
213
- },
214
- method: {
215
- provided: methodName || null,
216
- found: methodName ? methodFound : null,
217
- skipped: methodCheckSkipped ? true : null,
218
- error: methodCheckError,
219
- },
220
- project_root: {
221
- provided: projectRoot || null,
222
- detected: projectRootDetected,
223
- },
224
- recommendations: [
225
- !exists ? 'Check file_path or provide project_root.' : null,
226
- methodName && !methodFound ? 'Confirm method_name or use the exact symbol spelling.' : null,
227
- methodCheckError ? `Failed to read file for method check: ${methodCheckError}` : null,
228
- ].filter(Boolean),
281
+ ok: preflight.ok,
282
+ code: preflight.code,
283
+ message: preflight.message,
284
+ details: preflight.details,
285
+ project_root: preflight.projectRoot,
286
+ recommendations: preflight.ok ? [] : ['Fix the preflight error and retry the requested TNG tool.'],
229
287
  };
230
288
  return {
231
289
  content: [
@@ -236,6 +294,128 @@ server.registerTool('validate_request', {
236
294
  ],
237
295
  };
238
296
  });
297
+ const PROJECT_WIDE_PHRASES = ['project', 'entire project', 'whole project', 'project-wide', 'all files', 'scan all', 'repository'];
298
+ const INTENT_DEFINITIONS = [
299
+ {
300
+ tool: 'generate_test',
301
+ strongPhrases: ['generate tests', 'generate test', 'write tests', 'write test', 'create specs', 'create spec', 'add test coverage'],
302
+ phrases: ['test coverage', 'unit test', 'integration test', 'spec file', 'test suite'],
303
+ tokens: ['test', 'tests', 'spec', 'coverage'],
304
+ negativeTokens: ['diagram', 'trace', 'impact', 'dead', 'unused', 'usage'],
305
+ },
306
+ {
307
+ tool: 'impact_method',
308
+ strongPhrases: ['blast radius', 'regression check', 'change impact', 'what breaks'],
309
+ phrases: ['regression', 'breaking change', 'breakage', 'impact analysis', 'risk analysis'],
310
+ tokens: ['impact', 'regression', 'breaks', 'breakage', 'risk'],
311
+ negativeTokens: ['diagram', 'trace'],
312
+ },
313
+ {
314
+ tool: 'audit_method',
315
+ strongPhrases: ['code review', 'review this method', 'audit this method', 'find issues'],
316
+ phrases: ['audit', 'review', 'analyze', 'inspect', 'security issues', 'code smells', 'bugs'],
317
+ tokens: ['audit', 'review', 'analyze', 'inspect', 'security', 'vulnerability', 'bug', 'issue', 'smell'],
318
+ negativeTokens: ['diagram', 'flowchart'],
319
+ },
320
+ {
321
+ tool: 'xray_method',
322
+ strongPhrases: ['xray diagram', 'x-ray diagram', 'flow chart', 'visualize flow'],
323
+ phrases: ['diagram', 'flowchart', 'mermaid', 'visual', 'xray', 'x-ray'],
324
+ tokens: ['diagram', 'flowchart', 'mermaid', 'visualize'],
325
+ negativeTokens: ['unused', 'dead'],
326
+ },
327
+ {
328
+ tool: 'trace_method',
329
+ strongPhrases: ['execution trace', 'trace method', 'step by step', 'step-by-step'],
330
+ phrases: ['trace execution', 'runtime flow', 'execution path', 'what happens when'],
331
+ tokens: ['trace', 'execution', 'runtime', 'debug'],
332
+ negativeTokens: ['diagram', 'mermaid', 'unused'],
333
+ },
334
+ {
335
+ tool: 'find_call_sites',
336
+ strongPhrases: ['where is this used', 'who calls', 'find references', 'find usages', 'call sites'],
337
+ phrases: ['callsite', 'usages', 'usage', 'references', 'where used'],
338
+ tokens: ['used', 'usage', 'usages', 'reference', 'references', 'calls', 'called'],
339
+ negativeTokens: ['unused', 'dead', 'unreachable', 'orphaned'],
340
+ },
341
+ {
342
+ tool: 'detect_clones',
343
+ strongPhrases: ['duplicate code', 'find duplicates', 'copy paste code'],
344
+ phrases: ['clone', 'clones', 'copy-paste', 'repeated code', 'similar code'],
345
+ tokens: ['clone', 'duplicate', 'duplicates', 'copy', 'paste', 'similar'],
346
+ },
347
+ {
348
+ tool: 'detect_deadcode',
349
+ strongPhrases: ['dead code', 'unused code', 'never used', 'not used', 'unreachable code'],
350
+ phrases: ['unused import', 'unused variable', 'unused function', 'orphaned code', 'stale code'],
351
+ tokens: ['dead', 'unused', 'unreachable', 'orphaned', 'stale', 'ununsed'],
352
+ negativeTokens: ['where', 'who', 'calls', 'reference', 'references'],
353
+ },
354
+ ];
355
+ function normalizeIntentText(text) {
356
+ const normalized = text
357
+ .toLowerCase()
358
+ .replace(/[`"'()[\]{}:;,.!?]/g, ' ')
359
+ .replace(/[-_/]/g, ' ')
360
+ .replace(/\s+/g, ' ')
361
+ .trim();
362
+ const typoMap = {
363
+ reviewe: 'review',
364
+ reviee: 'review',
365
+ reviwe: 'review',
366
+ audti: 'audit',
367
+ audut: 'audit',
368
+ anlyze: 'analyze',
369
+ analze: 'analyze',
370
+ trcae: 'trace',
371
+ ununsed: 'unused',
372
+ };
373
+ return normalized
374
+ .split(' ')
375
+ .map((token) => typoMap[token] || token)
376
+ .join(' ');
377
+ }
378
+ function singularizeToken(token) {
379
+ if (token.endsWith('ies') && token.length > 3)
380
+ return `${token.slice(0, -3)}y`;
381
+ if (token.endsWith('ses') && token.length > 3)
382
+ return token.slice(0, -2);
383
+ if (token.endsWith('s') && token.length > 3)
384
+ return token.slice(0, -1);
385
+ return token;
386
+ }
387
+ function scoreIntent(normalized) {
388
+ const tokens = normalized.split(' ').filter(Boolean);
389
+ const tokenSet = new Set(tokens.flatMap((token) => [token, singularizeToken(token)]));
390
+ const hasWhereUsedPattern = /\bwhere\s+is\s+.+\s+used\b/.test(normalized) || /\bwhere\s+.+\s+used\b/.test(normalized);
391
+ const hasWhoCallsPattern = /\bwho\s+calls\b/.test(normalized);
392
+ const hasUnusedPattern = /\b(unused|ununsed|dead|unreachable|never used|not used|orphaned)\b/.test(normalized);
393
+ const scores = INTENT_DEFINITIONS.map((definition) => {
394
+ let score = 0;
395
+ for (const phrase of definition.strongPhrases) {
396
+ if (normalized.includes(phrase))
397
+ score += 3.0;
398
+ }
399
+ for (const phrase of definition.phrases) {
400
+ if (normalized.includes(phrase))
401
+ score += 1.5;
402
+ }
403
+ for (const token of definition.tokens) {
404
+ if (tokenSet.has(token))
405
+ score += 0.7;
406
+ }
407
+ for (const token of definition.negativeTokens || []) {
408
+ if (tokenSet.has(token))
409
+ score -= 0.8;
410
+ }
411
+ if (definition.tool === 'find_call_sites' && (hasWhereUsedPattern || hasWhoCallsPattern))
412
+ score += 2.2;
413
+ if (definition.tool === 'detect_deadcode' && hasUnusedPattern)
414
+ score += 2.2;
415
+ return { tool: definition.tool, score };
416
+ });
417
+ return scores.sort((a, b) => b.score - a.score);
418
+ }
239
419
  // 8. Request Router - Map Natural Language to a Tool
240
420
  server.registerTool('route_tng_request', {
241
421
  description: 'Route a natural-language request to the best TNG tool and list required arguments.',
@@ -244,20 +424,15 @@ server.registerTool('route_tng_request', {
244
424
  },
245
425
  }, async (args) => {
246
426
  const rawRequest = String(args?.request || '');
247
- const request = rawRequest.toLowerCase();
248
- const matchAny = (phrases) => phrases.some((p) => request.includes(p));
249
- const toolOrder = [
250
- { tool: 'generate_test', phrases: ['write test', 'write tests', 'generate test', 'generate tests', 'create specs', 'add test coverage', 'test coverage'] },
251
- { tool: 'impact_method', phrases: ['regression', 'regression check', 'impact', 'blast radius', 'breaking change', 'breakage', 'what breaks', 'change impact'] },
252
- { tool: 'audit_method', phrases: ['audit', 'review', 'analyze', 'inspect', 'check', 'security', 'vulnerab', 'bug', 'issue', 'code smell'] },
253
- { tool: 'xray_method', phrases: ['diagram', 'flowchart', 'visual', 'mermaid', 'xray', 'x-ray', 'flow'] },
254
- { tool: 'trace_method', phrases: ['trace', 'step through', 'step-by-step', 'execution', 'debug flow'] },
255
- { tool: 'find_call_sites', phrases: ['call sites', 'callsite', 'usages', 'usage', 'where is this used', 'who calls', 'find references'] },
256
- { tool: 'detect_clones', phrases: ['clone', 'duplicate', 'copy-paste', 'repeated code', 'similar code'] },
257
- { tool: 'detect_deadcode', phrases: ['dead code', 'unused', 'unreachable', 'unused import', 'unused variable'] },
258
- ];
259
- const matched = toolOrder.find((entry) => matchAny(entry.phrases));
260
- const tool = matched?.tool || null;
427
+ const normalizedRequest = normalizeIntentText(rawRequest);
428
+ const ranked = scoreIntent(normalizedRequest);
429
+ const top = ranked[0];
430
+ const second = ranked[1];
431
+ const ambiguous = !!top && !!second && top.score >= 1.5 && second.score >= 1.5 && (top.score - second.score) < 1.0;
432
+ const tool = top && top.score >= 1.5 ? top.tool : null;
433
+ const confidence = !tool ? 0.0 : ambiguous ? 0.55 : Math.min(0.97, 0.6 + top.score / 10);
434
+ const matchAny = (phrases) => phrases.some((p) => normalizedRequest.includes(p));
435
+ const wantsAllDeadCode = matchAny(PROJECT_WIDE_PHRASES);
261
436
  const backticks = [...rawRequest.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
262
437
  let file_path = null;
263
438
  let method_name = null;
@@ -279,31 +454,50 @@ server.registerTool('route_tng_request', {
279
454
  }
280
455
  }
281
456
  const required = [];
282
- if (tool === 'audit_method' || tool === 'generate_test' || tool === 'xray_method' || tool === 'trace_method' || tool === 'impact_method' || tool === 'find_call_sites') {
457
+ if (tool === 'audit_method') {
458
+ required.push('method_name');
459
+ }
460
+ if (tool === 'generate_test' || tool === 'xray_method' || tool === 'trace_method' || tool === 'impact_method' || tool === 'find_call_sites') {
283
461
  required.push('file_path', 'method_name');
284
462
  }
285
463
  if (tool === 'detect_clones') {
286
464
  required.push('file_path');
287
465
  }
288
466
  if (tool === 'detect_deadcode') {
289
- const wantsAll = matchAny(['project', 'entire project', 'whole project', 'project-wide', 'all files', 'scan all', 'repository']);
290
- if (!wantsAll) {
467
+ if (!wantsAllDeadCode) {
291
468
  required.push('file_path');
292
469
  }
293
470
  }
294
471
  const missing = required.filter((key) => (key === 'file_path' ? !file_path : !method_name));
472
+ const alternatives = ranked.filter((entry) => entry.score >= 1.5).slice(0, 3);
473
+ const reason = !tool
474
+ ? 'No strong tool intent match. Ask the user to clarify the task.'
475
+ : ambiguous
476
+ ? `Ambiguous between ${top.tool} and ${second.tool}; defaulting to ${top.tool}.`
477
+ : `Matched weighted intent patterns for ${tool}.`;
295
478
  const response = {
296
479
  tool,
297
- reason: tool ? `Matched keywords for ${tool}.` : 'No matching keywords found. Ask the user to clarify the intent.',
480
+ confidence,
481
+ reason,
482
+ alternatives,
298
483
  suggested_args: {
299
484
  file_path,
300
485
  method_name,
301
- all: tool === 'detect_deadcode' && matchAny(['project', 'entire project', 'whole project', 'project-wide', 'all files', 'scan all', 'repository']),
486
+ all: tool === 'detect_deadcode' && wantsAllDeadCode,
302
487
  note: tool === 'generate_test' || tool === 'audit_method'
303
488
  ? 'For JS/Python, test_type may be required (e.g., utility, react_component, express_handler).'
304
489
  : undefined,
305
490
  },
306
491
  missing_required_args: missing,
492
+ suggested_invocation: tool
493
+ ? {
494
+ tool,
495
+ args: {
496
+ ...(file_path ? { file_path } : {}),
497
+ ...(method_name ? { method_name } : {}),
498
+ },
499
+ }
500
+ : null,
307
501
  };
308
502
  return {
309
503
  content: [
@@ -314,6 +508,7 @@ server.registerTool('route_tng_request', {
314
508
  ],
315
509
  };
316
510
  });
511
+ registerPrompts(server);
317
512
  // Start the MCP server
318
513
  async function main() {
319
514
  const transport = new StdioServerTransport();