@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.
- package/dist/index.js +283 -88
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +8 -0
- package/dist/init.js.map +1 -1
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +63 -0
- package/dist/prompts.js.map +1 -0
- package/dist/tools/audit.d.ts +5 -2
- package/dist/tools/audit.d.ts.map +1 -1
- package/dist/tools/audit.js +115 -147
- package/dist/tools/audit.js.map +1 -1
- package/dist/tools/call_sites.d.ts +4 -1
- package/dist/tools/call_sites.d.ts.map +1 -1
- package/dist/tools/call_sites.js +16 -23
- package/dist/tools/call_sites.js.map +1 -1
- package/dist/tools/clones.d.ts +4 -1
- package/dist/tools/clones.d.ts.map +1 -1
- package/dist/tools/clones.js +19 -24
- package/dist/tools/clones.js.map +1 -1
- package/dist/tools/deadcode.d.ts +4 -1
- package/dist/tools/deadcode.d.ts.map +1 -1
- package/dist/tools/deadcode.js +19 -24
- package/dist/tools/deadcode.js.map +1 -1
- package/dist/tools/generate.d.ts +4 -1
- package/dist/tools/generate.d.ts.map +1 -1
- package/dist/tools/generate.js +19 -24
- package/dist/tools/generate.js.map +1 -1
- package/dist/tools/impact.d.ts +4 -1
- package/dist/tools/impact.d.ts.map +1 -1
- package/dist/tools/impact.js +19 -24
- package/dist/tools/impact.js.map +1 -1
- package/dist/tools/method_resolver.d.ts +20 -0
- package/dist/tools/method_resolver.d.ts.map +1 -0
- package/dist/tools/method_resolver.js +138 -0
- package/dist/tools/method_resolver.js.map +1 -0
- package/dist/tools/trace.d.ts +4 -1
- package/dist/tools/trace.d.ts.map +1 -1
- package/dist/tools/trace.js +19 -24
- package/dist/tools/trace.js.map +1 -1
- package/dist/tools/utils.d.ts +36 -1
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +305 -17
- package/dist/tools/utils.js.map +1 -1
- package/dist/tools/xray.d.ts +4 -1
- package/dist/tools/xray.d.ts.map +1 -1
- package/dist/tools/xray.js +21 -23
- package/dist/tools/xray.js.map +1 -1
- 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 {
|
|
15
|
-
import {
|
|
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.
|
|
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 (
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
114
|
-
server.registerTool('analyze_method', { description: 'Alias of audit_method. Use for "analyze this method".', inputSchema: auditSchema }, async (args) => await
|
|
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
|
|
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
|
|
119
|
-
server.registerTool('write_tests', { description: 'Alias of generate_test. Use for "write tests".', inputSchema: generateSchema }, async (args) => await
|
|
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
|
|
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
|
|
124
|
-
server.registerTool('visualize_flow', { description: 'Alias of xray_method. Use for "visualize flow".', inputSchema: methodSchema }, async (args) => await
|
|
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
|
|
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
|
|
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
|
|
131
|
-
server.registerTool('impact_audit', { description: 'Alias of impact_method. Use for "regression check".', inputSchema: impactSchema }, async (args) => await
|
|
132
|
-
server.registerTool('regression_check', { description: 'Alias of impact_method. Use for "regression check".', inputSchema: impactSchema }, async (args) => await
|
|
133
|
-
server.registerTool('blast_radius', { description: 'Alias of impact_method. Use for "blast radius check".', inputSchema: impactSchema }, async (args) => await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
480
|
+
confidence,
|
|
481
|
+
reason,
|
|
482
|
+
alternatives,
|
|
298
483
|
suggested_args: {
|
|
299
484
|
file_path,
|
|
300
485
|
method_name,
|
|
301
|
-
all: tool === 'detect_deadcode' &&
|
|
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();
|