@nielspeter/sonarlint-mcp-server 0.1.3 → 0.2.2
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 +40 -0
- package/README.md +6 -12
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +44 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.js +115 -1330
- package/dist/index.js.map +1 -1
- package/dist/resources/session.d.ts +18 -0
- package/dist/resources/session.d.ts.map +1 -0
- package/dist/resources/session.js +84 -0
- package/dist/resources/session.js.map +1 -0
- package/dist/sloop-bridge.d.ts +0 -3
- package/dist/sloop-bridge.d.ts.map +1 -1
- package/dist/sloop-bridge.js +0 -19
- package/dist/sloop-bridge.js.map +1 -1
- package/dist/state.d.ts +19 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +25 -0
- package/dist/state.js.map +1 -0
- package/dist/tools/analyze-content.d.ts +7 -0
- package/dist/tools/analyze-content.d.ts.map +1 -0
- package/dist/tools/analyze-content.js +78 -0
- package/dist/tools/analyze-content.js.map +1 -0
- package/dist/tools/analyze-file.d.ts +7 -0
- package/dist/tools/analyze-file.d.ts.map +1 -0
- package/dist/tools/analyze-file.js +66 -0
- package/dist/tools/analyze-file.js.map +1 -0
- package/dist/tools/analyze-files.d.ts +7 -0
- package/dist/tools/analyze-files.d.ts.map +1 -0
- package/dist/tools/analyze-files.js +106 -0
- package/dist/tools/analyze-files.js.map +1 -0
- package/dist/tools/analyze-project.d.ts +7 -0
- package/dist/tools/analyze-project.d.ts.map +1 -0
- package/dist/tools/analyze-project.js +109 -0
- package/dist/tools/analyze-project.js.map +1 -0
- package/dist/tools/apply-all-quick-fixes.d.ts +7 -0
- package/dist/tools/apply-all-quick-fixes.d.ts.map +1 -0
- package/dist/tools/apply-all-quick-fixes.js +166 -0
- package/dist/tools/apply-all-quick-fixes.js.map +1 -0
- package/dist/tools/apply-quick-fix.d.ts +7 -0
- package/dist/tools/apply-quick-fix.d.ts.map +1 -0
- package/dist/tools/apply-quick-fix.js +113 -0
- package/dist/tools/apply-quick-fix.js.map +1 -0
- package/dist/tools/health-check.d.ts +7 -0
- package/dist/tools/health-check.d.ts.map +1 -0
- package/dist/tools/health-check.js +113 -0
- package/dist/tools/health-check.js.map +1 -0
- package/dist/tools/list-active-rules.d.ts +7 -0
- package/dist/tools/list-active-rules.d.ts.map +1 -0
- package/dist/tools/list-active-rules.js +48 -0
- package/dist/tools/list-active-rules.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/filesystem.d.ts +8 -0
- package/dist/utils/filesystem.d.ts.map +1 -0
- package/dist/utils/filesystem.js +74 -0
- package/dist/utils/filesystem.js.map +1 -0
- package/dist/utils/formatting.d.ts +13 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/formatting.js +94 -0
- package/dist/utils/formatting.js.map +1 -0
- package/dist/utils/language.d.ts +12 -0
- package/dist/utils/language.d.ts.map +1 -0
- package/dist/utils/language.js +44 -0
- package/dist/utils/language.js.map +1 -0
- package/dist/utils/scope.d.ts +8 -0
- package/dist/utils/scope.d.ts.map +1 -0
- package/dist/utils/scope.js +30 -0
- package/dist/utils/scope.js.map +1 -0
- package/dist/utils/sloop.d.ts +10 -0
- package/dist/utils/sloop.d.ts.map +1 -0
- package/dist/utils/sloop.js +39 -0
- package/dist/utils/sloop.js.map +1 -0
- package/dist/utils/transforms.d.ts +23 -0
- package/dist/utils/transforms.d.ts.map +1 -0
- package/dist/utils/transforms.js +64 -0
- package/dist/utils/transforms.js.map +1 -0
- package/package.json +10 -7
- package/scripts/setup-sonarlint.sh +115 -39
- package/dist/sonarlint-bridge.d.ts +0 -33
- package/dist/sonarlint-bridge.d.ts.map +0 -1
- package/dist/sonarlint-bridge.js +0 -91
- package/dist/sonarlint-bridge.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1357 +1,152 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
userMessage;
|
|
17
|
-
recoverable;
|
|
18
|
-
constructor(message, userMessage, recoverable = false) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.userMessage = userMessage;
|
|
21
|
-
this.recoverable = recoverable;
|
|
22
|
-
this.name = 'SloopError';
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
// Global SLOOP bridge instance (lazy initialized)
|
|
26
|
-
let sloopBridge = null;
|
|
27
|
-
const scopeMap = new Map(); // projectRoot -> scopeId
|
|
28
|
-
// File system tracking (removed - using didUpdateFileSystem instead)
|
|
29
|
-
// Session storage for analysis results (for MCP resources)
|
|
30
|
-
const sessionResults = new Map();
|
|
31
|
-
const batchResults = new Map();
|
|
32
|
-
// Server start time for uptime tracking
|
|
33
|
-
const serverStartTime = Date.now();
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { handleToolError } from "./errors.js";
|
|
6
|
+
import { registerResources } from "./resources/session.js";
|
|
7
|
+
import { handleAnalyzeFile } from "./tools/analyze-file.js";
|
|
8
|
+
import { handleAnalyzeFiles } from "./tools/analyze-files.js";
|
|
9
|
+
import { handleAnalyzeContent } from "./tools/analyze-content.js";
|
|
10
|
+
import { handleListActiveRules } from "./tools/list-active-rules.js";
|
|
11
|
+
import { handleHealthCheck } from "./tools/health-check.js";
|
|
12
|
+
import { handleAnalyzeProject } from "./tools/analyze-project.js";
|
|
13
|
+
import { handleApplyQuickFix } from "./tools/apply-quick-fix.js";
|
|
14
|
+
import { handleApplyAllQuickFixes } from "./tools/apply-all-quick-fixes.js";
|
|
15
|
+
import { getSloopBridge } from "./state.js";
|
|
34
16
|
// Initialize the MCP server
|
|
35
|
-
const server = new
|
|
17
|
+
const server = new McpServer({
|
|
36
18
|
name: "sonarlint-mcp-server",
|
|
37
19
|
version: "1.0.0",
|
|
38
|
-
}, {
|
|
39
|
-
capabilities: {
|
|
40
|
-
tools: {},
|
|
41
|
-
resources: {},
|
|
42
|
-
},
|
|
43
20
|
});
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
sloopBridge = new SloopBridge(PACKAGE_ROOT);
|
|
55
|
-
await sloopBridge.connect();
|
|
56
|
-
console.error("[MCP] SLOOP bridge initialized successfully");
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
throw new SloopError(`Failed to initialize SLOOP: ${error}`, "Failed to start SonarLint backend. Please check that Java is installed and try again.", true);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return sloopBridge;
|
|
63
|
-
}
|
|
64
|
-
// Helper: Get or create configuration scope for a project
|
|
65
|
-
function getOrCreateScope(filePath) {
|
|
66
|
-
const projectRoot = dirname(filePath);
|
|
67
|
-
const scopeId = scopeMap.get(projectRoot);
|
|
68
|
-
if (scopeId) {
|
|
69
|
-
return scopeId;
|
|
70
|
-
}
|
|
71
|
-
// Create new scope ID based on project root hash
|
|
72
|
-
const hash = createHash('md5').update(projectRoot).digest('hex').substring(0, 8);
|
|
73
|
-
const newScopeId = `scope-${hash}`;
|
|
74
|
-
console.error(`[MCP] Creating new configuration scope: ${newScopeId} for ${projectRoot}`);
|
|
75
|
-
// Add scope to SLOOP
|
|
76
|
-
if (sloopBridge) {
|
|
77
|
-
sloopBridge.addConfigurationScope(newScopeId, {
|
|
78
|
-
name: `Project: ${projectRoot}`,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
scopeMap.set(projectRoot, newScopeId);
|
|
82
|
-
return newScopeId;
|
|
83
|
-
}
|
|
84
|
-
// Helper: Detect language from file extension
|
|
85
|
-
function detectLanguage(filePath) {
|
|
86
|
-
const ext = extname(filePath).toLowerCase();
|
|
87
|
-
const languageMap = {
|
|
88
|
-
'.js': 'javascript',
|
|
89
|
-
'.jsx': 'javascript',
|
|
90
|
-
'.ts': 'typescript',
|
|
91
|
-
'.tsx': 'typescript',
|
|
92
|
-
'.py': 'python',
|
|
93
|
-
'.java': 'java',
|
|
94
|
-
'.go': 'go',
|
|
95
|
-
'.php': 'php',
|
|
96
|
-
'.rb': 'ruby',
|
|
97
|
-
'.html': 'html',
|
|
98
|
-
'.css': 'css',
|
|
99
|
-
'.xml': 'xml',
|
|
100
|
-
};
|
|
101
|
-
return languageMap[ext] || 'unknown';
|
|
102
|
-
}
|
|
103
|
-
// Helper: Map language name to SLOOP Language enum
|
|
104
|
-
function languageToEnum(language) {
|
|
105
|
-
const enumMap = {
|
|
106
|
-
'javascript': 'JS',
|
|
107
|
-
'typescript': 'TS',
|
|
108
|
-
'python': 'PYTHON',
|
|
109
|
-
'java': 'JAVA',
|
|
110
|
-
'go': 'GO',
|
|
111
|
-
'php': 'PHP',
|
|
112
|
-
'ruby': 'RUBY',
|
|
113
|
-
'html': 'HTML',
|
|
114
|
-
'css': 'CSS',
|
|
115
|
-
'xml': 'XML',
|
|
116
|
-
};
|
|
117
|
-
return enumMap[language] || language.toUpperCase();
|
|
118
|
-
}
|
|
119
|
-
// Helper: Notify SLOOP that file system was updated (proper cache invalidation)
|
|
120
|
-
async function notifyFileSystemChanged(filePath, configScopeId) {
|
|
121
|
-
const uri = `file://${filePath}`;
|
|
122
|
-
// Get project root from scopeMap (reverse lookup)
|
|
123
|
-
let projectRoot;
|
|
124
|
-
for (const [root, scopeId] of scopeMap.entries()) {
|
|
125
|
-
if (scopeId === configScopeId) {
|
|
126
|
-
projectRoot = root;
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// If no project root found, use file's directory
|
|
131
|
-
if (!projectRoot) {
|
|
132
|
-
projectRoot = dirname(filePath);
|
|
133
|
-
}
|
|
134
|
-
const relativePath = relative(projectRoot, filePath);
|
|
21
|
+
// Register tool: analyze_file
|
|
22
|
+
server.registerTool('analyze_file', {
|
|
23
|
+
description: "Analyze a single file for code quality issues, bugs, and security vulnerabilities using SonarLint rules. Returns detailed issues with line numbers, severity levels, and quick fixes.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
filePath: z.string().describe("Absolute path to the file to analyze (e.g., /path/to/file.js)"),
|
|
26
|
+
minSeverity: z.enum(["INFO", "MINOR", "MAJOR", "CRITICAL", "BLOCKER"]).optional().describe("Minimum severity level to include. Filters out issues below this level. Default: INFO (show all)"),
|
|
27
|
+
excludeRules: z.array(z.string()).optional().describe("List of rule IDs to exclude (e.g., ['typescript:S1135', 'javascript:S125'])"),
|
|
28
|
+
},
|
|
29
|
+
}, async (args) => {
|
|
135
30
|
try {
|
|
136
|
-
|
|
137
|
-
// Detect language from file extension
|
|
138
|
-
const language = detectLanguage(filePath);
|
|
139
|
-
const languageEnum = languageToEnum(language);
|
|
140
|
-
// CRITICAL: Tell SLOOP the file is "open" so it will re-analyze on file system updates
|
|
141
|
-
// Without this, SLOOP ignores changes to "closed" files
|
|
142
|
-
bridge.sendNotification('file/didOpenFile', {
|
|
143
|
-
configurationScopeId: configScopeId,
|
|
144
|
-
fileUri: uri
|
|
145
|
-
});
|
|
146
|
-
console.error(`[FS] Marked file as open: ${filePath}`);
|
|
147
|
-
// Read the actual file content to pass to SLOOP
|
|
148
|
-
// This ensures SLOOP gets the latest content instead of reading from its cache
|
|
149
|
-
const fileContent = readFileSync(filePath, 'utf-8');
|
|
150
|
-
// Build ClientFileDto
|
|
151
|
-
// Pass BOTH fsPath and content - fromDto will call setDirty(content) which takes precedence
|
|
152
|
-
const clientFileDto = {
|
|
153
|
-
uri,
|
|
154
|
-
ideRelativePath: relativePath,
|
|
155
|
-
configScopeId,
|
|
156
|
-
isTest: null,
|
|
157
|
-
charset: 'UTF-8',
|
|
158
|
-
fsPath: filePath, // Provide fsPath for analyzers that need it
|
|
159
|
-
content: fileContent, // Providing content calls setDirty(), which takes precedence over fsPath
|
|
160
|
-
detectedLanguage: languageEnum, // e.g., "JS", "TS", "PYTHON"
|
|
161
|
-
isUserDefined: true // CRITICAL: Must be true for SLOOP to analyze!
|
|
162
|
-
};
|
|
163
|
-
// Send file/didUpdateFileSystem notification
|
|
164
|
-
bridge.sendNotification('file/didUpdateFileSystem', {
|
|
165
|
-
addedFiles: [],
|
|
166
|
-
changedFiles: [clientFileDto],
|
|
167
|
-
removedFiles: []
|
|
168
|
-
});
|
|
169
|
-
console.error(`[FS] Notified file system update:`);
|
|
170
|
-
console.error(`[FS] URI: ${uri}`);
|
|
171
|
-
console.error(`[FS] Language: ${languageEnum}`);
|
|
172
|
-
console.error(`[FS] Relative: ${relativePath}`);
|
|
173
|
-
console.error(`[FS] ConfigScopeId: ${configScopeId}`);
|
|
174
|
-
console.error(`[FS] Content length: ${fileContent.length} chars`);
|
|
175
|
-
console.error(`[FS] First 100 chars: ${fileContent.substring(0, 100)}`);
|
|
31
|
+
return await handleAnalyzeFile(args);
|
|
176
32
|
}
|
|
177
|
-
catch (
|
|
178
|
-
|
|
179
|
-
// Don't throw - this is not critical
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// Helper: Transform raw SLOOP issues to simplified format
|
|
183
|
-
function transformSloopIssues(rawIssues) {
|
|
184
|
-
return rawIssues.map((issue) => {
|
|
185
|
-
// Debug: Log raw issue to understand structure
|
|
186
|
-
if (!issue.startLine && !issue.textRange?.startLine) {
|
|
187
|
-
console.error('[DEBUG] Issue missing line info:', JSON.stringify(issue, null, 2).substring(0, 500));
|
|
188
|
-
}
|
|
189
|
-
const transformed = {
|
|
190
|
-
line: issue.textRange?.startLine || issue.startLine || 1,
|
|
191
|
-
column: issue.textRange?.startLineOffset || issue.startColumn || 0,
|
|
192
|
-
endLine: issue.textRange?.endLine || issue.endLine || issue.textRange?.startLine || issue.startLine || 1,
|
|
193
|
-
endColumn: issue.textRange?.endLineOffset || issue.endColumn || issue.textRange?.startLineOffset || issue.startColumn || 0,
|
|
194
|
-
severity: issue.severity || 'MAJOR',
|
|
195
|
-
rule: issue.ruleKey || 'unknown',
|
|
196
|
-
ruleDescription: issue.ruleDescriptionContextKey || '',
|
|
197
|
-
message: issue.primaryMessage || issue.message || 'No description',
|
|
198
|
-
};
|
|
199
|
-
// Add quick fix if available
|
|
200
|
-
if (issue.quickFixes && issue.quickFixes.length > 0) {
|
|
201
|
-
const firstFix = issue.quickFixes[0];
|
|
202
|
-
const fileEdits = firstFix.inputFileEdits || firstFix.fileEdits || [];
|
|
203
|
-
transformed.quickFix = {
|
|
204
|
-
description: firstFix.message || 'Apply fix',
|
|
205
|
-
edits: fileEdits.flatMap((fileEdit) => (fileEdit.textEdits || []).map((edit) => ({
|
|
206
|
-
startLine: edit.range?.startLine || 1,
|
|
207
|
-
startColumn: edit.range?.startLineOffset || 0,
|
|
208
|
-
endLine: edit.range?.endLine || 1,
|
|
209
|
-
endColumn: edit.range?.endLineOffset || 0,
|
|
210
|
-
newText: edit.newText || '',
|
|
211
|
-
}))),
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
return transformed;
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
// Helper: Create analysis summary
|
|
218
|
-
function createSummary(issues, rulesChecked) {
|
|
219
|
-
const summary = {
|
|
220
|
-
total: issues.length,
|
|
221
|
-
bySeverity: {
|
|
222
|
-
blocker: 0,
|
|
223
|
-
critical: 0,
|
|
224
|
-
major: 0,
|
|
225
|
-
minor: 0,
|
|
226
|
-
info: 0,
|
|
227
|
-
},
|
|
228
|
-
rulesChecked,
|
|
229
|
-
};
|
|
230
|
-
for (const issue of issues) {
|
|
231
|
-
const severity = issue.severity.toLowerCase();
|
|
232
|
-
if (severity in summary.bySeverity) {
|
|
233
|
-
summary.bySeverity[severity]++;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return summary;
|
|
237
|
-
}
|
|
238
|
-
// Helper: Format analysis result for display
|
|
239
|
-
function formatAnalysisResult(result) {
|
|
240
|
-
const { filePath, language, issues, summary } = result;
|
|
241
|
-
let output = `# Analysis Results: ${filePath}\n\n`;
|
|
242
|
-
output += `**Language**: ${language}\n`;
|
|
243
|
-
output += `**Rules Checked**: ${summary.rulesChecked}\n`;
|
|
244
|
-
output += `**Total Issues**: ${summary.total}\n\n`;
|
|
245
|
-
if (summary.total === 0) {
|
|
246
|
-
output += "✅ No issues found!\n";
|
|
247
|
-
return output;
|
|
248
|
-
}
|
|
249
|
-
// Severity breakdown
|
|
250
|
-
output += `## Issues by Severity\n\n`;
|
|
251
|
-
if (summary.bySeverity.blocker > 0)
|
|
252
|
-
output += `- 🔴 **BLOCKER**: ${summary.bySeverity.blocker}\n`;
|
|
253
|
-
if (summary.bySeverity.critical > 0)
|
|
254
|
-
output += `- 🟠 **CRITICAL**: ${summary.bySeverity.critical}\n`;
|
|
255
|
-
if (summary.bySeverity.major > 0)
|
|
256
|
-
output += `- 🟡 **MAJOR**: ${summary.bySeverity.major}\n`;
|
|
257
|
-
if (summary.bySeverity.minor > 0)
|
|
258
|
-
output += `- 🔵 **MINOR**: ${summary.bySeverity.minor}\n`;
|
|
259
|
-
if (summary.bySeverity.info > 0)
|
|
260
|
-
output += `- ⚪ **INFO**: ${summary.bySeverity.info}\n`;
|
|
261
|
-
output += `\n`;
|
|
262
|
-
// Detailed issues
|
|
263
|
-
output += `## Detailed Issues\n\n`;
|
|
264
|
-
// Sort by line number
|
|
265
|
-
const sortedIssues = [...issues].sort((a, b) => a.line - b.line);
|
|
266
|
-
for (const issue of sortedIssues) {
|
|
267
|
-
output += `### Line ${issue.line}:${issue.column} - ${issue.severity}\n\n`;
|
|
268
|
-
output += `**Rule**: \`${issue.rule}\`\n\n`;
|
|
269
|
-
output += `**Message**: ${issue.message}\n\n`;
|
|
270
|
-
if (issue.quickFix) {
|
|
271
|
-
output += `**Quick Fix Available**: ${issue.quickFix.description}\n\n`;
|
|
272
|
-
}
|
|
273
|
-
output += `---\n\n`;
|
|
274
|
-
}
|
|
275
|
-
return output;
|
|
276
|
-
}
|
|
277
|
-
// Helper: Format batch analysis result
|
|
278
|
-
function formatBatchAnalysisResult(result) {
|
|
279
|
-
const { files, summary } = result;
|
|
280
|
-
let output = `# Batch Analysis Results\n\n`;
|
|
281
|
-
output += `**Total Files**: ${summary.totalFiles}\n`;
|
|
282
|
-
output += `**Files with Issues**: ${summary.filesWithIssues}\n`;
|
|
283
|
-
output += `**Total Issues**: ${summary.totalIssues}\n\n`;
|
|
284
|
-
// Overall severity breakdown
|
|
285
|
-
output += `## Overall Issues by Severity\n\n`;
|
|
286
|
-
if (summary.bySeverity.blocker > 0)
|
|
287
|
-
output += `- 🔴 **BLOCKER**: ${summary.bySeverity.blocker}\n`;
|
|
288
|
-
if (summary.bySeverity.critical > 0)
|
|
289
|
-
output += `- 🟠 **CRITICAL**: ${summary.bySeverity.critical}\n`;
|
|
290
|
-
if (summary.bySeverity.major > 0)
|
|
291
|
-
output += `- 🟡 **MAJOR**: ${summary.bySeverity.major}\n`;
|
|
292
|
-
if (summary.bySeverity.minor > 0)
|
|
293
|
-
output += `- 🔵 **MINOR**: ${summary.bySeverity.minor}\n`;
|
|
294
|
-
if (summary.bySeverity.info > 0)
|
|
295
|
-
output += `- ⚪ **INFO**: ${summary.bySeverity.info}\n`;
|
|
296
|
-
output += `\n`;
|
|
297
|
-
// File-by-file breakdown
|
|
298
|
-
output += `## Issues by File\n\n`;
|
|
299
|
-
for (const file of files) {
|
|
300
|
-
if (file.issueCount === 0) {
|
|
301
|
-
output += `### ✅ ${file.filePath}\n\nNo issues found.\n\n`;
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
output += `### ${file.filePath} (${file.issueCount} issue${file.issueCount > 1 ? 's' : ''})\n\n`;
|
|
305
|
-
// Group by severity
|
|
306
|
-
const bySeverity = {};
|
|
307
|
-
for (const issue of file.issues) {
|
|
308
|
-
if (!bySeverity[issue.severity]) {
|
|
309
|
-
bySeverity[issue.severity] = [];
|
|
310
|
-
}
|
|
311
|
-
bySeverity[issue.severity].push(issue);
|
|
312
|
-
}
|
|
313
|
-
for (const [severity, issues] of Object.entries(bySeverity)) {
|
|
314
|
-
output += `**${severity}** (${issues.length}):\n`;
|
|
315
|
-
for (const issue of issues) {
|
|
316
|
-
output += `- Line ${issue.line}: ${issue.message} [\`${issue.rule}\`]\n`;
|
|
317
|
-
}
|
|
318
|
-
output += `\n`;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return handleToolError(error);
|
|
321
35
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
{
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
properties: {
|
|
332
|
-
filePath: {
|
|
333
|
-
type: "string",
|
|
334
|
-
description: "Absolute path to the file to analyze (e.g., /path/to/file.js)",
|
|
335
|
-
},
|
|
336
|
-
minSeverity: {
|
|
337
|
-
type: "string",
|
|
338
|
-
enum: ["INFO", "MINOR", "MAJOR", "CRITICAL", "BLOCKER"],
|
|
339
|
-
description: "Minimum severity level to include. Filters out issues below this level. Default: INFO (show all)",
|
|
340
|
-
},
|
|
341
|
-
excludeRules: {
|
|
342
|
-
type: "array",
|
|
343
|
-
items: { type: "string" },
|
|
344
|
-
description: "List of rule IDs to exclude (e.g., ['typescript:S1135', 'javascript:S125'])",
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
required: ["filePath"],
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
name: "analyze_files",
|
|
352
|
-
description: "Analyze multiple files in batch for better performance. Returns issues grouped by file with an overall summary. Ideal for analyzing entire directories or project-wide scans.",
|
|
353
|
-
inputSchema: {
|
|
354
|
-
type: "object",
|
|
355
|
-
properties: {
|
|
356
|
-
filePaths: {
|
|
357
|
-
type: "array",
|
|
358
|
-
items: { type: "string" },
|
|
359
|
-
description: "Array of absolute file paths to analyze",
|
|
360
|
-
},
|
|
361
|
-
groupByFile: {
|
|
362
|
-
type: "boolean",
|
|
363
|
-
description: "Group issues by file in output (default: true)",
|
|
364
|
-
default: true,
|
|
365
|
-
},
|
|
366
|
-
minSeverity: {
|
|
367
|
-
type: "string",
|
|
368
|
-
enum: ["INFO", "MINOR", "MAJOR", "CRITICAL", "BLOCKER"],
|
|
369
|
-
description: "Minimum severity level to include. Filters out issues below this level. Default: INFO (show all)",
|
|
370
|
-
},
|
|
371
|
-
excludeRules: {
|
|
372
|
-
type: "array",
|
|
373
|
-
items: { type: "string" },
|
|
374
|
-
description: "List of rule IDs to exclude (e.g., ['typescript:S1135', 'javascript:S125'])",
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
required: ["filePaths"],
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
name: "analyze_content",
|
|
382
|
-
description: "Analyze code content without requiring a saved file. Useful for analyzing unsaved changes, code snippets, or generated code. Creates a temporary file for analysis.",
|
|
383
|
-
inputSchema: {
|
|
384
|
-
type: "object",
|
|
385
|
-
properties: {
|
|
386
|
-
content: {
|
|
387
|
-
type: "string",
|
|
388
|
-
description: "The code content to analyze",
|
|
389
|
-
},
|
|
390
|
-
language: {
|
|
391
|
-
type: "string",
|
|
392
|
-
enum: ["javascript", "typescript", "python", "java", "go", "php", "ruby"],
|
|
393
|
-
description: "Programming language of the content",
|
|
394
|
-
},
|
|
395
|
-
fileName: {
|
|
396
|
-
type: "string",
|
|
397
|
-
description: "Optional filename for context (e.g., 'MyComponent.tsx')",
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
|
-
required: ["content", "language"],
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
{
|
|
404
|
-
name: "list_active_rules",
|
|
405
|
-
description: "List all active SonarLint rules, optionally filtered by language. Shows which rules are being used to analyze code.",
|
|
406
|
-
inputSchema: {
|
|
407
|
-
type: "object",
|
|
408
|
-
properties: {
|
|
409
|
-
language: {
|
|
410
|
-
type: "string",
|
|
411
|
-
enum: ["javascript", "typescript", "python", "java", "go", "php", "ruby"],
|
|
412
|
-
description: "Filter rules by language (optional)",
|
|
413
|
-
},
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
},
|
|
417
|
-
{
|
|
418
|
-
name: "health_check",
|
|
419
|
-
description: "Check the health and status of the SonarLint MCP server. Returns backend status, plugin information, cache statistics, and performance metrics.",
|
|
420
|
-
inputSchema: {
|
|
421
|
-
type: "object",
|
|
422
|
-
properties: {},
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
{
|
|
426
|
-
name: "analyze_project",
|
|
427
|
-
description: "Scan an entire project directory for code quality issues. Recursively finds all supported source files and analyzes them in batch. Excludes common non-source directories (node_modules, dist, build, etc.).",
|
|
428
|
-
inputSchema: {
|
|
429
|
-
type: "object",
|
|
430
|
-
properties: {
|
|
431
|
-
projectPath: {
|
|
432
|
-
type: "string",
|
|
433
|
-
description: "Absolute path to the project directory to scan",
|
|
434
|
-
},
|
|
435
|
-
maxFiles: {
|
|
436
|
-
type: "number",
|
|
437
|
-
description: "Maximum number of files to analyze (default: 100, prevents overwhelming output)",
|
|
438
|
-
default: 100,
|
|
439
|
-
},
|
|
440
|
-
minSeverity: {
|
|
441
|
-
type: "string",
|
|
442
|
-
enum: ["INFO", "MINOR", "MAJOR", "CRITICAL", "BLOCKER"],
|
|
443
|
-
description: "Minimum severity level to include. Filters out issues below this level. Default: INFO (show all)",
|
|
444
|
-
},
|
|
445
|
-
excludeRules: {
|
|
446
|
-
type: "array",
|
|
447
|
-
items: { type: "string" },
|
|
448
|
-
description: "List of rule IDs to exclude (e.g., ['typescript:S1135', 'javascript:S125'])",
|
|
449
|
-
},
|
|
450
|
-
includePatterns: {
|
|
451
|
-
type: "array",
|
|
452
|
-
items: { type: "string" },
|
|
453
|
-
description: "File glob patterns to include (e.g., ['src/**/*.ts', 'lib/**/*.js']). Default: all supported extensions",
|
|
454
|
-
},
|
|
455
|
-
},
|
|
456
|
-
required: ["projectPath"],
|
|
457
|
-
},
|
|
458
|
-
},
|
|
459
|
-
{
|
|
460
|
-
name: "apply_quick_fix",
|
|
461
|
-
description: "Apply a quick fix for ONE SPECIFIC ISSUE at a time. Fixes only the single issue identified by filePath + line + rule. To fix multiple issues, call this tool multiple times (once per issue). The file is modified directly.",
|
|
462
|
-
inputSchema: {
|
|
463
|
-
type: "object",
|
|
464
|
-
properties: {
|
|
465
|
-
filePath: {
|
|
466
|
-
type: "string",
|
|
467
|
-
description: "Absolute path to the file to fix",
|
|
468
|
-
},
|
|
469
|
-
line: {
|
|
470
|
-
type: "number",
|
|
471
|
-
description: "Line number of the issue",
|
|
472
|
-
},
|
|
473
|
-
rule: {
|
|
474
|
-
type: "string",
|
|
475
|
-
description: "Rule ID (e.g., 'javascript:S3504')",
|
|
476
|
-
},
|
|
477
|
-
},
|
|
478
|
-
required: ["filePath", "line", "rule"],
|
|
479
|
-
},
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
name: "apply_all_quick_fixes",
|
|
483
|
-
description: "Apply ALL available quick fixes for a file in one operation. Automatically identifies and fixes all issues that have SonarLint quick fixes available. More efficient than calling apply_quick_fix multiple times. Returns summary of what was fixed and what issues remain (issues without quick fixes must be fixed manually).",
|
|
484
|
-
inputSchema: {
|
|
485
|
-
type: "object",
|
|
486
|
-
properties: {
|
|
487
|
-
filePath: {
|
|
488
|
-
type: "string",
|
|
489
|
-
description: "Absolute path to the file to fix",
|
|
490
|
-
},
|
|
491
|
-
},
|
|
492
|
-
required: ["filePath"],
|
|
493
|
-
},
|
|
36
|
+
});
|
|
37
|
+
// Register tool: analyze_files
|
|
38
|
+
server.registerTool('analyze_files', {
|
|
39
|
+
description: "Analyze multiple files in batch for better performance. Returns issues grouped by file with an overall summary. Ideal for analyzing entire directories or project-wide scans.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
filePaths: z.array(z.string()).describe("Array of absolute file paths to analyze"),
|
|
42
|
+
groupByFile: z.boolean().optional().default(true).describe("Group issues by file in output (default: true)"),
|
|
43
|
+
minSeverity: z.enum(["INFO", "MINOR", "MAJOR", "CRITICAL", "BLOCKER"]).optional().describe("Minimum severity level to include. Filters out issues below this level. Default: INFO (show all)"),
|
|
44
|
+
excludeRules: z.array(z.string()).optional().describe("List of rule IDs to exclude (e.g., ['typescript:S1135', 'javascript:S125'])"),
|
|
494
45
|
},
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
tools,
|
|
499
|
-
}));
|
|
500
|
-
// Register MCP resources
|
|
501
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
502
|
-
const resources = [];
|
|
503
|
-
// Add session analysis results as resources
|
|
504
|
-
for (const [filePath, result] of sessionResults) {
|
|
505
|
-
const resourceId = `analysis-${createHash('md5').update(filePath).digest('hex').substring(0, 8)}`;
|
|
506
|
-
resources.push({
|
|
507
|
-
uri: `sonarlint://session/${resourceId}`,
|
|
508
|
-
name: `Analysis: ${basename(filePath)}`,
|
|
509
|
-
description: `${result.summary.total} issues found`,
|
|
510
|
-
mimeType: "application/json",
|
|
511
|
-
});
|
|
46
|
+
}, async (args) => {
|
|
47
|
+
try {
|
|
48
|
+
return await handleAnalyzeFiles(args);
|
|
512
49
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
resources.push({
|
|
516
|
-
uri: `sonarlint://batch/${batchId}`,
|
|
517
|
-
name: `Batch Analysis: ${result.summary.totalFiles} files`,
|
|
518
|
-
description: `${result.summary.totalIssues} total issues`,
|
|
519
|
-
mimeType: "application/json",
|
|
520
|
-
});
|
|
50
|
+
catch (error) {
|
|
51
|
+
return handleToolError(error);
|
|
521
52
|
}
|
|
522
|
-
return { resources };
|
|
523
53
|
});
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
mimeType: "application/json",
|
|
536
|
-
text: JSON.stringify(result, null, 2),
|
|
537
|
-
}],
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
}
|
|
54
|
+
// Register tool: analyze_content
|
|
55
|
+
server.registerTool('analyze_content', {
|
|
56
|
+
description: "Analyze code content without requiring a saved file. Useful for analyzing unsaved changes, code snippets, or generated code. Creates a temporary file for analysis.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
content: z.string().describe("The code content to analyze"),
|
|
59
|
+
language: z.enum(["javascript", "typescript", "python", "java", "go", "php", "ruby"]).describe("Programming language of the content"),
|
|
60
|
+
fileName: z.string().optional().describe("Optional filename for context (e.g., 'MyComponent.tsx')"),
|
|
61
|
+
},
|
|
62
|
+
}, async (args) => {
|
|
63
|
+
try {
|
|
64
|
+
return await handleAnalyzeContent(args);
|
|
541
65
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const result = batchResults.get(batchId);
|
|
545
|
-
if (result) {
|
|
546
|
-
return {
|
|
547
|
-
contents: [{
|
|
548
|
-
uri,
|
|
549
|
-
mimeType: "application/json",
|
|
550
|
-
text: JSON.stringify(result, null, 2),
|
|
551
|
-
}],
|
|
552
|
-
};
|
|
553
|
-
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return handleToolError(error);
|
|
554
68
|
}
|
|
555
|
-
throw new Error(`Resource not found: ${uri}`);
|
|
556
69
|
});
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
// Detect language
|
|
565
|
-
const language = detectLanguage(filePath);
|
|
566
|
-
if (language === 'unknown') {
|
|
567
|
-
const ext = extname(filePath);
|
|
568
|
-
throw new SloopError(`Unknown language for ${filePath}`, `No analyzer available for ${ext} files. Supported extensions: .js, .jsx, .ts, .tsx, .py, .java, .go, .php, .rb, .html, .css, .xml`, false);
|
|
569
|
-
}
|
|
570
|
-
// Ensure SLOOP is initialized
|
|
571
|
-
const bridge = await ensureSloopBridge();
|
|
572
|
-
// Get or create scope
|
|
573
|
-
const scopeId = getOrCreateScope(filePath);
|
|
574
|
-
console.error(`[MCP] Analyzing file: ${filePath}`);
|
|
575
|
-
console.error(`[MCP] Scope: ${scopeId}, Language: ${language}`);
|
|
576
|
-
// Analyze the file
|
|
577
|
-
console.error(`[MCP] Calling analyzeFilesAndTrack...`);
|
|
578
|
-
const rawResult = await bridge.analyzeFilesAndTrack(scopeId, [filePath]);
|
|
579
|
-
console.error(`[MCP] analyzeFilesAndTrack returned`);
|
|
580
|
-
// Extract issues from raw result
|
|
581
|
-
const rawIssues = rawResult.rawIssues || [];
|
|
582
|
-
console.error(`[MCP] Found ${rawIssues.length} raw issues`);
|
|
583
|
-
// Transform to simplified format
|
|
584
|
-
let issues = transformSloopIssues(rawIssues);
|
|
585
|
-
// Apply filtering if requested
|
|
586
|
-
if (minSeverity) {
|
|
587
|
-
const severityOrder = { INFO: 0, MINOR: 1, MAJOR: 2, CRITICAL: 3, BLOCKER: 4 };
|
|
588
|
-
const minLevel = severityOrder[minSeverity];
|
|
589
|
-
issues = issues.filter(issue => (severityOrder[issue.severity] || 0) >= minLevel);
|
|
590
|
-
}
|
|
591
|
-
if (excludeRules && excludeRules.length > 0) {
|
|
592
|
-
issues = issues.filter(issue => !excludeRules.includes(issue.rule));
|
|
593
|
-
}
|
|
594
|
-
// Create result
|
|
595
|
-
const result = {
|
|
596
|
-
filePath,
|
|
597
|
-
language,
|
|
598
|
-
issues,
|
|
599
|
-
summary: createSummary(issues, 265), // TODO: Get actual rule count from SLOOP
|
|
600
|
-
};
|
|
601
|
-
// Store in session for MCP resources
|
|
602
|
-
sessionResults.set(filePath, result);
|
|
603
|
-
// Format for display
|
|
604
|
-
const formattedResult = formatAnalysisResult(result);
|
|
605
|
-
return {
|
|
606
|
-
content: [
|
|
607
|
-
{
|
|
608
|
-
type: "text",
|
|
609
|
-
text: formattedResult,
|
|
610
|
-
},
|
|
611
|
-
],
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
async function handleAnalyzeFiles(args) {
|
|
615
|
-
const { filePaths, groupByFile = true, minSeverity, excludeRules } = args;
|
|
616
|
-
if (!Array.isArray(filePaths) || filePaths.length === 0) {
|
|
617
|
-
throw new SloopError("No files provided", "Please provide at least one file path to analyze.", false);
|
|
618
|
-
}
|
|
619
|
-
// Validate all files exist
|
|
620
|
-
const missingFiles = filePaths.filter(fp => !existsSync(fp));
|
|
621
|
-
if (missingFiles.length > 0) {
|
|
622
|
-
throw new SloopError(`Files not found: ${missingFiles.join(', ')}`, `The following files do not exist:\n${missingFiles.map(f => `- ${f}`).join('\n')}`, false);
|
|
623
|
-
}
|
|
624
|
-
console.error(`[MCP] Batch analyzing ${filePaths.length} files...`);
|
|
625
|
-
// Group files by project root for scope management
|
|
626
|
-
const filesByScope = new Map();
|
|
627
|
-
for (const filePath of filePaths) {
|
|
628
|
-
const scopeId = getOrCreateScope(filePath);
|
|
629
|
-
if (!filesByScope.has(scopeId)) {
|
|
630
|
-
filesByScope.set(scopeId, []);
|
|
631
|
-
}
|
|
632
|
-
filesByScope.get(scopeId).push(filePath);
|
|
633
|
-
}
|
|
634
|
-
// Ensure SLOOP is initialized
|
|
635
|
-
const bridge = await ensureSloopBridge();
|
|
636
|
-
// Analyze each scope
|
|
637
|
-
const allResults = [];
|
|
638
|
-
for (const [scopeId, scopeFiles] of filesByScope) {
|
|
639
|
-
console.error(`[MCP] Analyzing ${scopeFiles.length} files in scope ${scopeId}`);
|
|
640
|
-
const rawResult = await bridge.analyzeFilesAndTrack(scopeId, scopeFiles);
|
|
641
|
-
const rawIssues = rawResult.rawIssues || [];
|
|
642
|
-
// Group issues by file
|
|
643
|
-
const issuesByFile = new Map();
|
|
644
|
-
for (const issue of rawIssues) {
|
|
645
|
-
const fileUri = issue.fileUri;
|
|
646
|
-
if (!issuesByFile.has(fileUri)) {
|
|
647
|
-
issuesByFile.set(fileUri, []);
|
|
648
|
-
}
|
|
649
|
-
issuesByFile.get(fileUri).push(issue);
|
|
650
|
-
}
|
|
651
|
-
// Create results for each file
|
|
652
|
-
for (const filePath of scopeFiles) {
|
|
653
|
-
const fileUri = `file://${filePath}`;
|
|
654
|
-
const fileIssues = issuesByFile.get(fileUri) || [];
|
|
655
|
-
let transformedIssues = transformSloopIssues(fileIssues);
|
|
656
|
-
// Apply filtering if requested
|
|
657
|
-
if (minSeverity) {
|
|
658
|
-
const severityOrder = { INFO: 0, MINOR: 1, MAJOR: 2, CRITICAL: 3, BLOCKER: 4 };
|
|
659
|
-
const minLevel = severityOrder[minSeverity];
|
|
660
|
-
transformedIssues = transformedIssues.filter(issue => (severityOrder[issue.severity] || 0) >= minLevel);
|
|
661
|
-
}
|
|
662
|
-
if (excludeRules && excludeRules.length > 0) {
|
|
663
|
-
transformedIssues = transformedIssues.filter(issue => !excludeRules.includes(issue.rule));
|
|
664
|
-
}
|
|
665
|
-
allResults.push({
|
|
666
|
-
filePath,
|
|
667
|
-
language: detectLanguage(filePath),
|
|
668
|
-
issueCount: transformedIssues.length,
|
|
669
|
-
issues: transformedIssues,
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
// Calculate overall summary
|
|
674
|
-
const overallSummary = {
|
|
675
|
-
totalFiles: allResults.length,
|
|
676
|
-
totalIssues: allResults.reduce((sum, r) => sum + r.issueCount, 0),
|
|
677
|
-
filesWithIssues: allResults.filter(r => r.issueCount > 0).length,
|
|
678
|
-
bySeverity: {
|
|
679
|
-
blocker: 0,
|
|
680
|
-
critical: 0,
|
|
681
|
-
major: 0,
|
|
682
|
-
minor: 0,
|
|
683
|
-
info: 0,
|
|
684
|
-
},
|
|
685
|
-
};
|
|
686
|
-
for (const result of allResults) {
|
|
687
|
-
for (const issue of result.issues) {
|
|
688
|
-
const severity = issue.severity.toLowerCase();
|
|
689
|
-
if (severity in overallSummary.bySeverity) {
|
|
690
|
-
overallSummary.bySeverity[severity]++;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
const batchResult = {
|
|
695
|
-
files: allResults,
|
|
696
|
-
summary: overallSummary,
|
|
697
|
-
};
|
|
698
|
-
// Store in batch results for MCP resources
|
|
699
|
-
const batchId = `batch-${Date.now()}`;
|
|
700
|
-
batchResults.set(batchId, batchResult);
|
|
701
|
-
const formattedResult = formatBatchAnalysisResult(batchResult);
|
|
702
|
-
return {
|
|
703
|
-
content: [
|
|
704
|
-
{
|
|
705
|
-
type: "text",
|
|
706
|
-
text: formattedResult,
|
|
707
|
-
},
|
|
708
|
-
],
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
async function handleAnalyzeContent(args) {
|
|
712
|
-
const { content, language, fileName } = args;
|
|
713
|
-
if (!content || content.trim().length === 0) {
|
|
714
|
-
throw new SloopError("Empty content", "Please provide non-empty content to analyze.", false);
|
|
715
|
-
}
|
|
716
|
-
// Generate filename with appropriate extension
|
|
717
|
-
const languageExtMap = {
|
|
718
|
-
'javascript': '.js',
|
|
719
|
-
'typescript': '.ts',
|
|
720
|
-
'python': '.py',
|
|
721
|
-
'java': '.java',
|
|
722
|
-
'go': '.go',
|
|
723
|
-
'php': '.php',
|
|
724
|
-
'ruby': '.rb',
|
|
725
|
-
};
|
|
726
|
-
const ext = languageExtMap[language] || '.txt';
|
|
727
|
-
const tempFileName = fileName || `.sonarlint-tmp-${Date.now()}${ext}`;
|
|
728
|
-
// Create temp file in project root so SLOOP's listFiles can find it
|
|
729
|
-
const tempFilePath = join(process.cwd(), tempFileName);
|
|
730
|
-
console.error(`[MCP] Analyzing content as ${language}, temp file: ${tempFilePath}`);
|
|
70
|
+
// Register tool: list_active_rules
|
|
71
|
+
server.registerTool('list_active_rules', {
|
|
72
|
+
description: "List all active SonarLint rules, optionally filtered by language. Shows which rules are being used to analyze code.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
language: z.enum(["javascript", "typescript", "python", "java", "go", "php", "ruby"]).optional().describe("Filter rules by language (optional)"),
|
|
75
|
+
},
|
|
76
|
+
}, async (args) => {
|
|
731
77
|
try {
|
|
732
|
-
|
|
733
|
-
const tempDir = dirname(tempFilePath);
|
|
734
|
-
if (!existsSync(tempDir)) {
|
|
735
|
-
const { mkdirSync } = await import('fs');
|
|
736
|
-
mkdirSync(tempDir, { recursive: true });
|
|
737
|
-
}
|
|
738
|
-
// Write content to temp file
|
|
739
|
-
writeFileSync(tempFilePath, content, 'utf-8');
|
|
740
|
-
// Ensure SLOOP is initialized
|
|
741
|
-
const bridge = await ensureSloopBridge();
|
|
742
|
-
// Get or create scope
|
|
743
|
-
const scopeId = getOrCreateScope(tempFilePath);
|
|
744
|
-
// Analyze the temp file
|
|
745
|
-
const rawResult = await bridge.analyzeFilesAndTrack(scopeId, [tempFilePath]);
|
|
746
|
-
// Extract issues from raw result
|
|
747
|
-
const rawIssues = rawResult.rawIssues || [];
|
|
748
|
-
console.error(`[MCP] Found ${rawIssues.length} raw issues in content`);
|
|
749
|
-
// Transform to simplified format
|
|
750
|
-
const issues = transformSloopIssues(rawIssues);
|
|
751
|
-
// Create result
|
|
752
|
-
const result = {
|
|
753
|
-
filePath: fileName || 'content',
|
|
754
|
-
language,
|
|
755
|
-
issues,
|
|
756
|
-
summary: createSummary(issues, 265),
|
|
757
|
-
};
|
|
758
|
-
// Format for display
|
|
759
|
-
const formattedResult = formatAnalysisResult(result);
|
|
760
|
-
return {
|
|
761
|
-
content: [
|
|
762
|
-
{
|
|
763
|
-
type: "text",
|
|
764
|
-
text: `${formattedResult}\n\n---\n*Note: Analyzed unsaved content*`,
|
|
765
|
-
},
|
|
766
|
-
],
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
finally {
|
|
770
|
-
// Clean up temp file
|
|
771
|
-
try {
|
|
772
|
-
if (existsSync(tempFilePath)) {
|
|
773
|
-
unlinkSync(tempFilePath);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
catch (cleanupError) {
|
|
777
|
-
console.error(`[MCP] Failed to clean up temp file: ${cleanupError}`);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
async function handleListActiveRules(args) {
|
|
782
|
-
const { language } = args;
|
|
783
|
-
console.error(`[MCP] Listing active rules${language ? ` for ${language}` : ''}`);
|
|
784
|
-
// TODO: Extract actual rules from SLOOP plugins via RPC
|
|
785
|
-
// For now, return a summary of known rules
|
|
786
|
-
let output = `# Active SonarLint Rules\n\n`;
|
|
787
|
-
if (!language || language === 'javascript' || language === 'typescript') {
|
|
788
|
-
output += `## JavaScript/TypeScript Rules\n\n`;
|
|
789
|
-
output += `**Total Rules**: 265\n\n`;
|
|
790
|
-
output += `### Rule Categories\n\n`;
|
|
791
|
-
output += `- **Code Smells**: Rules that detect maintainability issues\n`;
|
|
792
|
-
output += ` - \`S1481\`: Unused local variables\n`;
|
|
793
|
-
output += ` - \`S1854\`: Useless assignments\n`;
|
|
794
|
-
output += ` - \`S3504\`: Prefer let/const over var\n`;
|
|
795
|
-
output += ` - \`S107\`: Too many parameters\n`;
|
|
796
|
-
output += ` - \`S4144\`: Duplicate implementations\n`;
|
|
797
|
-
output += ` - \`S2589\`: Always-truthy expressions\n\n`;
|
|
798
|
-
output += `- **Bugs**: Rules that detect potential errors\n`;
|
|
799
|
-
output += ` - \`S2259\`: Null pointer dereference\n`;
|
|
800
|
-
output += ` - \`S3776\`: Cognitive complexity\n\n`;
|
|
801
|
-
output += `- **Security**: Rules that detect security vulnerabilities\n`;
|
|
802
|
-
output += ` - \`S5852\`: Regular expression DoS\n`;
|
|
803
|
-
output += ` - \`S2068\`: Hard-coded credentials\n\n`;
|
|
804
|
-
}
|
|
805
|
-
if (!language || language === 'python') {
|
|
806
|
-
output += `## Python Rules\n\n`;
|
|
807
|
-
output += `**Total Rules**: ~200\n\n`;
|
|
808
|
-
output += `### Rule Categories\n\n`;
|
|
809
|
-
output += `- **Code Smells**: Maintainability issues\n`;
|
|
810
|
-
output += ` - \`S1066\`: Nested if statements\n`;
|
|
811
|
-
output += ` - \`S1192\`: String literals duplicated\n\n`;
|
|
812
|
-
output += `- **Bugs**: Potential errors\n`;
|
|
813
|
-
output += ` - \`S5754\`: Unreachable code\n\n`;
|
|
814
|
-
output += `- **Security**: Security vulnerabilities\n`;
|
|
815
|
-
output += ` - \`S5659\`: Weak encryption\n\n`;
|
|
816
|
-
}
|
|
817
|
-
output += `\n---\n\n`;
|
|
818
|
-
output += `*Note: This is a summary of active rules. Full rule details are available at https://rules.sonarsource.com/*\n`;
|
|
819
|
-
return {
|
|
820
|
-
content: [
|
|
821
|
-
{
|
|
822
|
-
type: "text",
|
|
823
|
-
text: output,
|
|
824
|
-
},
|
|
825
|
-
],
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
async function handleHealthCheck() {
|
|
829
|
-
console.error(`[MCP] Running health check...`);
|
|
830
|
-
const uptimeMs = Date.now() - serverStartTime;
|
|
831
|
-
const uptimeSeconds = Math.floor(uptimeMs / 1000);
|
|
832
|
-
const uptimeMinutes = Math.floor(uptimeSeconds / 60);
|
|
833
|
-
const uptimeHours = Math.floor(uptimeMinutes / 60);
|
|
834
|
-
const memoryUsage = process.memoryUsage();
|
|
835
|
-
const memoryMB = Math.round(memoryUsage.heapUsed / 1024 / 1024);
|
|
836
|
-
// Check SLOOP status
|
|
837
|
-
const sloopStatus = sloopBridge ? "running" : "not started";
|
|
838
|
-
// Get plugin information
|
|
839
|
-
const pluginsDir = join(process.cwd(), "sonarlint-backend", "plugins");
|
|
840
|
-
const pluginsExist = existsSync(pluginsDir);
|
|
841
|
-
let plugins = [];
|
|
842
|
-
if (pluginsExist) {
|
|
843
|
-
const { readdirSync } = await import('fs');
|
|
844
|
-
const files = readdirSync(pluginsDir);
|
|
845
|
-
const jarFiles = files.filter(f => f.endsWith('.jar'));
|
|
846
|
-
for (const jarFile of jarFiles) {
|
|
847
|
-
// Parse plugin name and version from filename
|
|
848
|
-
const match = jarFile.match(/sonar-(\w+)-plugin-([\d.]+)\.jar/);
|
|
849
|
-
if (match) {
|
|
850
|
-
plugins.push({
|
|
851
|
-
name: match[1].charAt(0).toUpperCase() + match[1].slice(1),
|
|
852
|
-
version: match[2],
|
|
853
|
-
status: "active",
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
// Cache statistics
|
|
859
|
-
const cacheStats = {
|
|
860
|
-
sessionResults: sessionResults.size,
|
|
861
|
-
batchResults: batchResults.size,
|
|
862
|
-
};
|
|
863
|
-
const healthStatus = {
|
|
864
|
-
status: sloopStatus === "running" && pluginsExist ? "healthy" : "degraded",
|
|
865
|
-
version: "1.0.0 (Phase 3)",
|
|
866
|
-
uptime: {
|
|
867
|
-
milliseconds: uptimeMs,
|
|
868
|
-
seconds: uptimeSeconds,
|
|
869
|
-
minutes: uptimeMinutes,
|
|
870
|
-
hours: uptimeHours,
|
|
871
|
-
formatted: `${uptimeHours}h ${uptimeMinutes % 60}m ${uptimeSeconds % 60}s`,
|
|
872
|
-
},
|
|
873
|
-
backend: {
|
|
874
|
-
status: sloopStatus,
|
|
875
|
-
pluginsDirectory: pluginsExist ? "found" : "missing",
|
|
876
|
-
},
|
|
877
|
-
plugins,
|
|
878
|
-
memory: {
|
|
879
|
-
heapUsed: `${memoryMB}MB`,
|
|
880
|
-
heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)}MB`,
|
|
881
|
-
rss: `${Math.round(memoryUsage.rss / 1024 / 1024)}MB`,
|
|
882
|
-
},
|
|
883
|
-
cache: cacheStats,
|
|
884
|
-
tools: ["analyze_file", "analyze_files", "analyze_content", "list_active_rules", "health_check"],
|
|
885
|
-
features: [
|
|
886
|
-
"Session storage for multi-turn conversations",
|
|
887
|
-
"Batch analysis",
|
|
888
|
-
"Content analysis (unsaved files)",
|
|
889
|
-
"MCP resources",
|
|
890
|
-
"Quick fixes support",
|
|
891
|
-
],
|
|
892
|
-
};
|
|
893
|
-
let output = `# SonarLint MCP Server Health Check\n\n`;
|
|
894
|
-
output += `**Status**: ${healthStatus.status === "healthy" ? "✅ Healthy" : "⚠️ Degraded"}\n`;
|
|
895
|
-
output += `**Version**: ${healthStatus.version}\n`;
|
|
896
|
-
output += `**Uptime**: ${healthStatus.uptime.formatted}\n\n`;
|
|
897
|
-
output += `## Backend Status\n\n`;
|
|
898
|
-
output += `- **SLOOP Backend**: ${healthStatus.backend.status}\n`;
|
|
899
|
-
output += `- **Plugins Directory**: ${healthStatus.backend.pluginsDirectory}\n\n`;
|
|
900
|
-
if (plugins.length > 0) {
|
|
901
|
-
output += `## Active Plugins\n\n`;
|
|
902
|
-
for (const plugin of plugins) {
|
|
903
|
-
output += `- **${plugin.name}**: v${plugin.version} (${plugin.status})\n`;
|
|
904
|
-
}
|
|
905
|
-
output += `\n`;
|
|
906
|
-
}
|
|
907
|
-
output += `## Memory Usage\n\n`;
|
|
908
|
-
output += `- **Heap Used**: ${healthStatus.memory.heapUsed}\n`;
|
|
909
|
-
output += `- **Heap Total**: ${healthStatus.memory.heapTotal}\n`;
|
|
910
|
-
output += `- **RSS**: ${healthStatus.memory.rss}\n\n`;
|
|
911
|
-
output += `## Cache Statistics\n\n`;
|
|
912
|
-
output += `- **Session Results**: ${healthStatus.cache.sessionResults} stored\n`;
|
|
913
|
-
output += `- **Batch Results**: ${healthStatus.cache.batchResults} stored\n\n`;
|
|
914
|
-
output += `## Available Tools\n\n`;
|
|
915
|
-
for (const tool of healthStatus.tools) {
|
|
916
|
-
output += `- ${tool}\n`;
|
|
917
|
-
}
|
|
918
|
-
output += `\n`;
|
|
919
|
-
output += `## Features\n\n`;
|
|
920
|
-
for (const feature of healthStatus.features) {
|
|
921
|
-
output += `- ${feature}\n`;
|
|
78
|
+
return await handleListActiveRules(args);
|
|
922
79
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
{
|
|
926
|
-
type: "text",
|
|
927
|
-
text: output,
|
|
928
|
-
},
|
|
929
|
-
],
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
async function handleAnalyzeProject(args) {
|
|
933
|
-
const { projectPath, maxFiles = 100, minSeverity, excludeRules, includePatterns } = args;
|
|
934
|
-
// Validate project path exists
|
|
935
|
-
if (!existsSync(projectPath)) {
|
|
936
|
-
throw new SloopError(`Project path not found: ${projectPath}`, `The directory ${projectPath} does not exist. Please check the path and try again.`, false);
|
|
937
|
-
}
|
|
938
|
-
const stats = statSync(projectPath);
|
|
939
|
-
if (!stats.isDirectory()) {
|
|
940
|
-
throw new SloopError(`Not a directory: ${projectPath}`, `The path ${projectPath} is not a directory. Please provide a directory path.`, false);
|
|
941
|
-
}
|
|
942
|
-
console.error(`[MCP] Scanning project: ${projectPath}`);
|
|
943
|
-
// Define supported extensions
|
|
944
|
-
const supportedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.go', '.php', '.rb', '.html', '.css', '.xml'];
|
|
945
|
-
// Directories to exclude
|
|
946
|
-
const excludeDirs = new Set([
|
|
947
|
-
'node_modules', 'dist', 'build', '.git', '.svn', '.hg',
|
|
948
|
-
'coverage', '.next', '.nuxt', 'out', 'target', 'bin',
|
|
949
|
-
'__pycache__', '.pytest_cache', '.mypy_cache', 'venv', '.venv'
|
|
950
|
-
]);
|
|
951
|
-
// Recursively find all source files
|
|
952
|
-
function findSourceFiles(dir, files = []) {
|
|
953
|
-
try {
|
|
954
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
955
|
-
for (const entry of entries) {
|
|
956
|
-
// Skip excluded directories
|
|
957
|
-
if (entry.isDirectory() && excludeDirs.has(entry.name)) {
|
|
958
|
-
continue;
|
|
959
|
-
}
|
|
960
|
-
const fullPath = join(dir, entry.name);
|
|
961
|
-
if (entry.isDirectory()) {
|
|
962
|
-
findSourceFiles(fullPath, files);
|
|
963
|
-
}
|
|
964
|
-
else if (entry.isFile()) {
|
|
965
|
-
const ext = extname(entry.name);
|
|
966
|
-
if (supportedExtensions.includes(ext)) {
|
|
967
|
-
// Check includePatterns if specified
|
|
968
|
-
if (includePatterns && includePatterns.length > 0) {
|
|
969
|
-
const relativePath = relative(projectPath, fullPath);
|
|
970
|
-
// Simple pattern matching (supports ** and *)
|
|
971
|
-
const matches = includePatterns.some(pattern => {
|
|
972
|
-
const regex = new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
|
|
973
|
-
return regex.test(relativePath);
|
|
974
|
-
});
|
|
975
|
-
if (matches) {
|
|
976
|
-
files.push(fullPath);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
files.push(fullPath);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
catch (err) {
|
|
987
|
-
console.error(`[MCP] Error scanning directory ${dir}:`, err);
|
|
988
|
-
}
|
|
989
|
-
return files;
|
|
990
|
-
}
|
|
991
|
-
const allFiles = findSourceFiles(projectPath);
|
|
992
|
-
console.error(`[MCP] Found ${allFiles.length} source files`);
|
|
993
|
-
if (allFiles.length === 0) {
|
|
994
|
-
return {
|
|
995
|
-
content: [
|
|
996
|
-
{
|
|
997
|
-
type: "text",
|
|
998
|
-
text: `No source files found in ${projectPath}.\n\nSupported extensions: ${supportedExtensions.join(', ')}`,
|
|
999
|
-
},
|
|
1000
|
-
],
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
// Limit number of files
|
|
1004
|
-
const filesToAnalyze = allFiles.slice(0, maxFiles);
|
|
1005
|
-
if (allFiles.length > maxFiles) {
|
|
1006
|
-
console.error(`[MCP] Limiting analysis to ${maxFiles} files (found ${allFiles.length})`);
|
|
1007
|
-
}
|
|
1008
|
-
// Use handleAnalyzeFiles to do the actual analysis
|
|
1009
|
-
const result = await handleAnalyzeFiles({
|
|
1010
|
-
filePaths: filesToAnalyze,
|
|
1011
|
-
groupByFile: true,
|
|
1012
|
-
minSeverity,
|
|
1013
|
-
excludeRules,
|
|
1014
|
-
});
|
|
1015
|
-
// Add project-specific context to the output
|
|
1016
|
-
const resultText = result.content[0].text;
|
|
1017
|
-
const projectSummary = `
|
|
1018
|
-
📦 Project Scan: ${basename(projectPath)}
|
|
1019
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1020
|
-
|
|
1021
|
-
Project Path: ${projectPath}
|
|
1022
|
-
Total Source Files: ${allFiles.length}
|
|
1023
|
-
Files Analyzed: ${filesToAnalyze.length}
|
|
1024
|
-
${allFiles.length > maxFiles ? `⚠️ Limited to ${maxFiles} files (use maxFiles parameter to adjust)\n` : ''}
|
|
1025
|
-
${resultText}
|
|
1026
|
-
`;
|
|
1027
|
-
return {
|
|
1028
|
-
content: [
|
|
1029
|
-
{
|
|
1030
|
-
type: "text",
|
|
1031
|
-
text: projectSummary,
|
|
1032
|
-
},
|
|
1033
|
-
],
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
async function handleApplyQuickFix(args) {
|
|
1037
|
-
const { filePath, line, rule } = args;
|
|
1038
|
-
console.error(`[MCP] Applying quick fix for ${rule} at ${filePath}:${line}`);
|
|
1039
|
-
// Validate file exists
|
|
1040
|
-
if (!existsSync(filePath)) {
|
|
1041
|
-
throw new SloopError(`File not found: ${filePath}`, `The file ${filePath} does not exist. Please check the path and try again.`, false);
|
|
1042
|
-
}
|
|
1043
|
-
// Re-analyze the file to get current issues with quick fixes
|
|
1044
|
-
const bridge = await ensureSloopBridge();
|
|
1045
|
-
const scopeId = getOrCreateScope(filePath);
|
|
1046
|
-
const rawResult = await bridge.analyzeFilesAndTrack(scopeId, [filePath]);
|
|
1047
|
-
const rawIssues = rawResult.rawIssues || [];
|
|
1048
|
-
// Find the issue at the specified line with the specified rule
|
|
1049
|
-
const targetIssue = rawIssues.find((issue) => {
|
|
1050
|
-
const issueLine = issue.textRange?.startLine || issue.startLine || 0;
|
|
1051
|
-
return issueLine === line && issue.ruleKey === rule;
|
|
1052
|
-
});
|
|
1053
|
-
if (!targetIssue) {
|
|
1054
|
-
throw new SloopError(`Issue not found`, `No issue found at line ${line} with rule ${rule}. The file may have changed since the last analysis.`, false);
|
|
1055
|
-
}
|
|
1056
|
-
if (!targetIssue.quickFixes || targetIssue.quickFixes.length === 0) {
|
|
1057
|
-
throw new SloopError(`No quick fix available`, `The issue at line ${line} (${rule}) does not have an automated quick fix available.`, false);
|
|
1058
|
-
}
|
|
1059
|
-
// Apply the first quick fix
|
|
1060
|
-
const quickFix = targetIssue.quickFixes[0];
|
|
1061
|
-
console.error('[DEBUG] Quick fix structure:', JSON.stringify(quickFix, null, 2).substring(0, 1000));
|
|
1062
|
-
// Write debug info to a file we can read
|
|
1063
|
-
writeFileSync('/tmp/quickfix-debug.json', JSON.stringify({
|
|
1064
|
-
targetIssue: {
|
|
1065
|
-
ruleKey: targetIssue.ruleKey,
|
|
1066
|
-
textRange: targetIssue.textRange,
|
|
1067
|
-
quickFixes: targetIssue.quickFixes
|
|
1068
|
-
},
|
|
1069
|
-
quickFix: quickFix
|
|
1070
|
-
}, null, 2), 'utf-8');
|
|
1071
|
-
let fileContent = readFileSync(filePath, 'utf-8');
|
|
1072
|
-
const lines = fileContent.split('\n');
|
|
1073
|
-
// Apply each edit in the quick fix
|
|
1074
|
-
const fileEdits = quickFix.inputFileEdits || quickFix.fileEdits || [];
|
|
1075
|
-
if (fileEdits.length > 0) {
|
|
1076
|
-
console.error(`[DEBUG] Found ${fileEdits.length} file edits`);
|
|
1077
|
-
for (const fileEdit of fileEdits) {
|
|
1078
|
-
if (fileEdit.textEdits) {
|
|
1079
|
-
console.error(`[DEBUG] Found ${fileEdit.textEdits.length} text edits`);
|
|
1080
|
-
// Sort edits in reverse order to maintain line numbers
|
|
1081
|
-
const sortedEdits = [...fileEdit.textEdits].sort((a, b) => {
|
|
1082
|
-
const aStart = a.range?.startLine || 0;
|
|
1083
|
-
const bStart = b.range?.startLine || 0;
|
|
1084
|
-
return bStart - aStart; // Reverse order
|
|
1085
|
-
});
|
|
1086
|
-
for (const edit of sortedEdits) {
|
|
1087
|
-
const startLine = (edit.range?.startLine || 1) - 1; // Convert to 0-based
|
|
1088
|
-
const startCol = edit.range?.startLineOffset || 0;
|
|
1089
|
-
const endLine = (edit.range?.endLine || startLine + 1) - 1;
|
|
1090
|
-
const endCol = edit.range?.endLineOffset || lines[endLine]?.length || 0;
|
|
1091
|
-
const newText = edit.newText || '';
|
|
1092
|
-
console.error(`[DEBUG] Applying edit at line ${startLine + 1}:${startCol} to ${endLine + 1}:${endCol}`);
|
|
1093
|
-
console.error(`[DEBUG] Old text: "${lines[startLine].substring(startCol, endCol)}"`);
|
|
1094
|
-
console.error(`[DEBUG] New text: "${newText}"`);
|
|
1095
|
-
// Apply the edit
|
|
1096
|
-
if (startLine === endLine) {
|
|
1097
|
-
const line = lines[startLine];
|
|
1098
|
-
lines[startLine] = line.substring(0, startCol) + newText + line.substring(endCol);
|
|
1099
|
-
console.error(`[DEBUG] Result: "${lines[startLine]}"`);
|
|
1100
|
-
}
|
|
1101
|
-
else {
|
|
1102
|
-
// Multi-line edit
|
|
1103
|
-
const firstLine = lines[startLine].substring(0, startCol) + newText;
|
|
1104
|
-
const lastLine = lines[endLine].substring(endCol);
|
|
1105
|
-
lines.splice(startLine, endLine - startLine + 1, firstLine + lastLine);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
else {
|
|
1112
|
-
console.error('[DEBUG] No fileEdits found in quick fix!');
|
|
80
|
+
catch (error) {
|
|
81
|
+
return handleToolError(error);
|
|
1113
82
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
83
|
+
});
|
|
84
|
+
// Register tool: health_check
|
|
85
|
+
server.registerTool('health_check', {
|
|
86
|
+
description: "Check the health and status of the SonarLint MCP server. Returns backend status, plugin information, cache statistics, and performance metrics.",
|
|
87
|
+
inputSchema: {},
|
|
88
|
+
}, async () => {
|
|
1119
89
|
try {
|
|
1120
|
-
|
|
1121
|
-
console.error(`[DEBUG] File written successfully`);
|
|
1122
|
-
}
|
|
1123
|
-
catch (err) {
|
|
1124
|
-
console.error(`[DEBUG] Error writing file:`, err);
|
|
1125
|
-
throw err;
|
|
90
|
+
return await handleHealthCheck();
|
|
1126
91
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
await notifyFileSystemChanged(filePath, scopeId);
|
|
1130
|
-
console.error(`[Cache] File system update notification sent, waiting for SLOOP to process...`);
|
|
1131
|
-
// CRITICAL: Give SLOOP time to process the file system notification
|
|
1132
|
-
// Without this delay, the next analysis request may arrive before SLOOP updates its registry
|
|
1133
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1134
|
-
return {
|
|
1135
|
-
content: [
|
|
1136
|
-
{
|
|
1137
|
-
type: "text",
|
|
1138
|
-
text: `✅ **Quick fix applied successfully**\n\nFile: ${filePath}\nLine: ${line}\nRule: ${rule}\nFix: ${quickFix.message || 'Applied automated fix'}\n\nThe file has been modified. You may want to re-analyze it to confirm the issue is resolved.`,
|
|
1139
|
-
},
|
|
1140
|
-
],
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
async function handleApplyAllQuickFixes(args) {
|
|
1144
|
-
const { filePath } = args;
|
|
1145
|
-
console.error(`[MCP] Applying all quick fixes for ${filePath}`);
|
|
1146
|
-
// Validate file exists
|
|
1147
|
-
if (!existsSync(filePath)) {
|
|
1148
|
-
throw new SloopError(`File not found: ${filePath}`, `The file ${filePath} does not exist. Please check the path and try again.`, false);
|
|
1149
|
-
}
|
|
1150
|
-
// Analyze the file to get all issues with quick fixes
|
|
1151
|
-
const bridge = await ensureSloopBridge();
|
|
1152
|
-
const scopeId = getOrCreateScope(filePath);
|
|
1153
|
-
const rawResult = await bridge.analyzeFilesAndTrack(scopeId, [filePath]);
|
|
1154
|
-
const rawIssues = rawResult.rawIssues || [];
|
|
1155
|
-
console.error(`[MCP] Found ${rawIssues.length} total issues`);
|
|
1156
|
-
// Filter issues that have quick fixes
|
|
1157
|
-
const issuesWithQuickFixes = rawIssues.filter((issue) => {
|
|
1158
|
-
const hasQuickFixes = issue.quickFixes && issue.quickFixes.length > 0;
|
|
1159
|
-
if (hasQuickFixes) {
|
|
1160
|
-
console.error(`[DEBUG] Issue at line ${issue.textRange?.startLine || issue.startLine}: ${issue.ruleKey} has ${issue.quickFixes.length} quick fixes`);
|
|
1161
|
-
}
|
|
1162
|
-
return hasQuickFixes;
|
|
1163
|
-
});
|
|
1164
|
-
console.error(`[MCP] Found ${issuesWithQuickFixes.length} issues with quick fixes`);
|
|
1165
|
-
if (issuesWithQuickFixes.length === 0) {
|
|
1166
|
-
return {
|
|
1167
|
-
content: [
|
|
1168
|
-
{
|
|
1169
|
-
type: "text",
|
|
1170
|
-
text: `ℹ️ **No quick fixes available**\n\nFile: ${filePath}\nTotal issues: ${rawIssues.length}\n\nNone of the issues in this file have automated quick fixes available. All issues must be fixed manually.`,
|
|
1171
|
-
},
|
|
1172
|
-
],
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
// Sort issues by line number (descending) to avoid line number shifts
|
|
1176
|
-
const sortedIssues = [...issuesWithQuickFixes].sort((a, b) => {
|
|
1177
|
-
const aLine = a.textRange?.startLine || a.startLine || 0;
|
|
1178
|
-
const bLine = b.textRange?.startLine || b.startLine || 0;
|
|
1179
|
-
return bLine - aLine; // Descending order
|
|
1180
|
-
});
|
|
1181
|
-
// Apply each quick fix
|
|
1182
|
-
const appliedFixes = [];
|
|
1183
|
-
const failedFixes = [];
|
|
1184
|
-
for (const issue of sortedIssues) {
|
|
1185
|
-
const line = issue.textRange?.startLine || issue.startLine || 0;
|
|
1186
|
-
const rule = issue.ruleKey;
|
|
1187
|
-
const quickFix = issue.quickFixes[0]; // Use first available quick fix
|
|
1188
|
-
console.error(`[MCP] Applying fix for ${rule} at line ${line}`);
|
|
1189
|
-
try {
|
|
1190
|
-
// Read current file content
|
|
1191
|
-
let fileContent = readFileSync(filePath, 'utf-8');
|
|
1192
|
-
const lines = fileContent.split('\n');
|
|
1193
|
-
// Apply the quick fix edits
|
|
1194
|
-
const fileEdits = quickFix.inputFileEdits || quickFix.fileEdits || [];
|
|
1195
|
-
if (fileEdits.length > 0) {
|
|
1196
|
-
for (const fileEdit of fileEdits) {
|
|
1197
|
-
if (fileEdit.textEdits) {
|
|
1198
|
-
// Sort edits in reverse order to maintain line numbers
|
|
1199
|
-
const sortedEdits = [...fileEdit.textEdits].sort((a, b) => {
|
|
1200
|
-
const aStart = a.range?.startLine || 0;
|
|
1201
|
-
const bStart = b.range?.startLine || 0;
|
|
1202
|
-
return bStart - aStart;
|
|
1203
|
-
});
|
|
1204
|
-
for (const edit of sortedEdits) {
|
|
1205
|
-
const startLine = (edit.range?.startLine || 1) - 1;
|
|
1206
|
-
const startCol = edit.range?.startLineOffset || 0;
|
|
1207
|
-
const endLine = (edit.range?.endLine || startLine + 1) - 1;
|
|
1208
|
-
const endCol = edit.range?.endLineOffset || lines[endLine]?.length || 0;
|
|
1209
|
-
const newText = edit.newText || '';
|
|
1210
|
-
if (startLine === endLine) {
|
|
1211
|
-
const currentLine = lines[startLine];
|
|
1212
|
-
lines[startLine] = currentLine.substring(0, startCol) + newText + currentLine.substring(endCol);
|
|
1213
|
-
}
|
|
1214
|
-
else {
|
|
1215
|
-
const firstLine = lines[startLine].substring(0, startCol) + newText;
|
|
1216
|
-
const lastLine = lines[endLine].substring(endCol);
|
|
1217
|
-
lines.splice(startLine, endLine - startLine + 1, firstLine + lastLine);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
// Write back to file
|
|
1224
|
-
fileContent = lines.join('\n');
|
|
1225
|
-
writeFileSync(filePath, fileContent, 'utf-8');
|
|
1226
|
-
appliedFixes.push({
|
|
1227
|
-
line,
|
|
1228
|
-
rule,
|
|
1229
|
-
message: quickFix.message || 'Applied automated fix',
|
|
1230
|
-
});
|
|
1231
|
-
console.error(`[MCP] Successfully applied fix for ${rule} at line ${line}`);
|
|
1232
|
-
}
|
|
1233
|
-
catch (error) {
|
|
1234
|
-
console.error(`[MCP] Failed to apply fix for ${rule} at line ${line}:`, error);
|
|
1235
|
-
failedFixes.push({
|
|
1236
|
-
line,
|
|
1237
|
-
rule,
|
|
1238
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
// Notify SLOOP about file changes
|
|
1243
|
-
console.error(`[Cache] Sending file system update notification...`);
|
|
1244
|
-
await notifyFileSystemChanged(filePath, scopeId);
|
|
1245
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1246
|
-
// Re-analyze to get remaining issues
|
|
1247
|
-
const finalResult = await bridge.analyzeFilesAndTrack(scopeId, [filePath]);
|
|
1248
|
-
const remainingIssues = finalResult.rawIssues || [];
|
|
1249
|
-
const transformedRemaining = transformSloopIssues(remainingIssues);
|
|
1250
|
-
// Format summary
|
|
1251
|
-
let summary = `✅ **Quick fixes applied**\n\n`;
|
|
1252
|
-
summary += `File: ${filePath}\n`;
|
|
1253
|
-
summary += `Applied: ${appliedFixes.length} fixes\n`;
|
|
1254
|
-
if (failedFixes.length > 0) {
|
|
1255
|
-
summary += `Failed: ${failedFixes.length} fixes\n`;
|
|
92
|
+
catch (error) {
|
|
93
|
+
return handleToolError(error);
|
|
1256
94
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
95
|
+
});
|
|
96
|
+
// Register tool: analyze_project
|
|
97
|
+
server.registerTool('analyze_project', {
|
|
98
|
+
description: "Scan an entire project directory for code quality issues. Recursively finds all supported source files and analyzes them in batch. Excludes common non-source directories (node_modules, dist, build, etc.).",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
projectPath: z.string().describe("Absolute path to the project directory to scan"),
|
|
101
|
+
maxFiles: z.number().optional().default(100).describe("Maximum number of files to analyze (default: 100, prevents overwhelming output)"),
|
|
102
|
+
minSeverity: z.enum(["INFO", "MINOR", "MAJOR", "CRITICAL", "BLOCKER"]).optional().describe("Minimum severity level to include. Filters out issues below this level. Default: INFO (show all)"),
|
|
103
|
+
excludeRules: z.array(z.string()).optional().describe("List of rule IDs to exclude (e.g., ['typescript:S1135', 'javascript:S125'])"),
|
|
104
|
+
includePatterns: z.array(z.string()).optional().describe("File glob patterns to include (e.g., ['src/**/*.ts', 'lib/**/*.js']). Default: all supported extensions"),
|
|
105
|
+
},
|
|
106
|
+
}, async (args) => {
|
|
107
|
+
try {
|
|
108
|
+
return await handleAnalyzeProject(args);
|
|
1264
109
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
for (const fail of failedFixes) {
|
|
1268
|
-
summary += `- Line ${fail.line}: ${fail.rule} - ${fail.error}\n`;
|
|
1269
|
-
}
|
|
1270
|
-
summary += `\n`;
|
|
110
|
+
catch (error) {
|
|
111
|
+
return handleToolError(error);
|
|
1271
112
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
for (const issue of issues) {
|
|
1285
|
-
summary += `- Line ${issue.line}: ${issue.rule} - ${issue.message}\n`;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
113
|
+
});
|
|
114
|
+
// Register tool: apply_quick_fix
|
|
115
|
+
server.registerTool('apply_quick_fix', {
|
|
116
|
+
description: "Apply a quick fix for ONE SPECIFIC ISSUE at a time. Fixes only the single issue identified by filePath + line + rule. To fix multiple issues, call this tool multiple times (once per issue). The file is modified directly.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
filePath: z.string().describe("Absolute path to the file to fix"),
|
|
119
|
+
line: z.number().describe("Line number of the issue"),
|
|
120
|
+
rule: z.string().describe("Rule ID (e.g., 'javascript:S3504')"),
|
|
121
|
+
},
|
|
122
|
+
}, async (args) => {
|
|
123
|
+
try {
|
|
124
|
+
return await handleApplyQuickFix(args);
|
|
1289
125
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
126
|
+
catch (error) {
|
|
127
|
+
return handleToolError(error);
|
|
1292
128
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1302
|
-
// Handle tool calls
|
|
1303
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1304
|
-
const { name, arguments: args } = request.params;
|
|
129
|
+
});
|
|
130
|
+
// Register tool: apply_all_quick_fixes
|
|
131
|
+
server.registerTool('apply_all_quick_fixes', {
|
|
132
|
+
description: "Apply ALL available quick fixes for a file in one operation. Automatically identifies and fixes all issues that have SonarLint quick fixes available. More efficient than calling apply_quick_fix multiple times. Returns summary of what was fixed and what issues remain (issues without quick fixes must be fixed manually).",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
filePath: z.string().describe("Absolute path to the file to fix"),
|
|
135
|
+
},
|
|
136
|
+
}, async (args) => {
|
|
1305
137
|
try {
|
|
1306
|
-
|
|
1307
|
-
case "analyze_file":
|
|
1308
|
-
return await handleAnalyzeFile(args);
|
|
1309
|
-
case "analyze_files":
|
|
1310
|
-
return await handleAnalyzeFiles(args);
|
|
1311
|
-
case "analyze_content":
|
|
1312
|
-
return await handleAnalyzeContent(args);
|
|
1313
|
-
case "list_active_rules":
|
|
1314
|
-
return await handleListActiveRules(args);
|
|
1315
|
-
case "health_check":
|
|
1316
|
-
return await handleHealthCheck();
|
|
1317
|
-
case "analyze_project":
|
|
1318
|
-
return await handleAnalyzeProject(args);
|
|
1319
|
-
case "apply_quick_fix":
|
|
1320
|
-
return await handleApplyQuickFix(args);
|
|
1321
|
-
case "apply_all_quick_fixes":
|
|
1322
|
-
return await handleApplyAllQuickFixes(args);
|
|
1323
|
-
default:
|
|
1324
|
-
throw new SloopError(`Unknown tool: ${name}`, `The tool '${name}' is not recognized. Available tools: analyze_file, analyze_files, analyze_content, list_active_rules, health_check, analyze_project, apply_quick_fix, apply_all_quick_fixes`, false);
|
|
1325
|
-
}
|
|
138
|
+
return await handleApplyAllQuickFixes(args);
|
|
1326
139
|
}
|
|
1327
140
|
catch (error) {
|
|
1328
|
-
|
|
1329
|
-
if (error instanceof SloopError) {
|
|
1330
|
-
return {
|
|
1331
|
-
content: [
|
|
1332
|
-
{
|
|
1333
|
-
type: "text",
|
|
1334
|
-
text: `❌ **Error**: ${error.userMessage}`,
|
|
1335
|
-
},
|
|
1336
|
-
],
|
|
1337
|
-
isError: true,
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1341
|
-
return {
|
|
1342
|
-
content: [
|
|
1343
|
-
{
|
|
1344
|
-
type: "text",
|
|
1345
|
-
text: `❌ **Error**: ${errorMessage}`,
|
|
1346
|
-
},
|
|
1347
|
-
],
|
|
1348
|
-
isError: true,
|
|
1349
|
-
};
|
|
141
|
+
return handleToolError(error);
|
|
1350
142
|
}
|
|
1351
143
|
});
|
|
144
|
+
// Register MCP resources
|
|
145
|
+
registerResources(server);
|
|
1352
146
|
// Graceful shutdown
|
|
1353
147
|
async function shutdown() {
|
|
1354
148
|
console.error("[MCP] Shutting down...");
|
|
149
|
+
const sloopBridge = getSloopBridge();
|
|
1355
150
|
if (sloopBridge) {
|
|
1356
151
|
try {
|
|
1357
152
|
await sloopBridge.disconnect();
|
|
@@ -1377,16 +172,6 @@ async function main() {
|
|
|
1377
172
|
console.error("[MCP] - Content analysis (unsaved files)");
|
|
1378
173
|
console.error("[MCP] - MCP resources for persistent results");
|
|
1379
174
|
console.error("[MCP] - Quick fixes support");
|
|
1380
|
-
// Initialize SLOOP backend eagerly to avoid first-request delays
|
|
1381
|
-
console.error("[MCP] Initializing SLOOP backend...");
|
|
1382
|
-
try {
|
|
1383
|
-
await ensureSloopBridge();
|
|
1384
|
-
console.error("[MCP] SLOOP backend ready");
|
|
1385
|
-
}
|
|
1386
|
-
catch (error) {
|
|
1387
|
-
console.error("[MCP] Warning: SLOOP backend initialization failed:", error);
|
|
1388
|
-
console.error("[MCP] Server will continue, but analysis requests will fail until backend starts");
|
|
1389
|
-
}
|
|
1390
175
|
const transport = new StdioServerTransport();
|
|
1391
176
|
await server.connect(transport);
|
|
1392
177
|
console.error("[MCP] Server ready! Waiting for tool calls...");
|