@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +6 -12
  3. package/dist/errors.d.ts +22 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/errors.js +44 -0
  6. package/dist/errors.js.map +1 -0
  7. package/dist/index.js +115 -1330
  8. package/dist/index.js.map +1 -1
  9. package/dist/resources/session.d.ts +18 -0
  10. package/dist/resources/session.d.ts.map +1 -0
  11. package/dist/resources/session.js +84 -0
  12. package/dist/resources/session.js.map +1 -0
  13. package/dist/sloop-bridge.d.ts +0 -3
  14. package/dist/sloop-bridge.d.ts.map +1 -1
  15. package/dist/sloop-bridge.js +0 -19
  16. package/dist/sloop-bridge.js.map +1 -1
  17. package/dist/state.d.ts +19 -0
  18. package/dist/state.d.ts.map +1 -0
  19. package/dist/state.js +25 -0
  20. package/dist/state.js.map +1 -0
  21. package/dist/tools/analyze-content.d.ts +7 -0
  22. package/dist/tools/analyze-content.d.ts.map +1 -0
  23. package/dist/tools/analyze-content.js +78 -0
  24. package/dist/tools/analyze-content.js.map +1 -0
  25. package/dist/tools/analyze-file.d.ts +7 -0
  26. package/dist/tools/analyze-file.d.ts.map +1 -0
  27. package/dist/tools/analyze-file.js +66 -0
  28. package/dist/tools/analyze-file.js.map +1 -0
  29. package/dist/tools/analyze-files.d.ts +7 -0
  30. package/dist/tools/analyze-files.d.ts.map +1 -0
  31. package/dist/tools/analyze-files.js +106 -0
  32. package/dist/tools/analyze-files.js.map +1 -0
  33. package/dist/tools/analyze-project.d.ts +7 -0
  34. package/dist/tools/analyze-project.d.ts.map +1 -0
  35. package/dist/tools/analyze-project.js +109 -0
  36. package/dist/tools/analyze-project.js.map +1 -0
  37. package/dist/tools/apply-all-quick-fixes.d.ts +7 -0
  38. package/dist/tools/apply-all-quick-fixes.d.ts.map +1 -0
  39. package/dist/tools/apply-all-quick-fixes.js +166 -0
  40. package/dist/tools/apply-all-quick-fixes.js.map +1 -0
  41. package/dist/tools/apply-quick-fix.d.ts +7 -0
  42. package/dist/tools/apply-quick-fix.d.ts.map +1 -0
  43. package/dist/tools/apply-quick-fix.js +113 -0
  44. package/dist/tools/apply-quick-fix.js.map +1 -0
  45. package/dist/tools/health-check.d.ts +7 -0
  46. package/dist/tools/health-check.d.ts.map +1 -0
  47. package/dist/tools/health-check.js +113 -0
  48. package/dist/tools/health-check.js.map +1 -0
  49. package/dist/tools/list-active-rules.d.ts +7 -0
  50. package/dist/tools/list-active-rules.d.ts.map +1 -0
  51. package/dist/tools/list-active-rules.js +48 -0
  52. package/dist/tools/list-active-rules.js.map +1 -0
  53. package/dist/types.d.ts +62 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +5 -0
  56. package/dist/types.js.map +1 -0
  57. package/dist/utils/filesystem.d.ts +8 -0
  58. package/dist/utils/filesystem.d.ts.map +1 -0
  59. package/dist/utils/filesystem.js +74 -0
  60. package/dist/utils/filesystem.js.map +1 -0
  61. package/dist/utils/formatting.d.ts +13 -0
  62. package/dist/utils/formatting.d.ts.map +1 -0
  63. package/dist/utils/formatting.js +94 -0
  64. package/dist/utils/formatting.js.map +1 -0
  65. package/dist/utils/language.d.ts +12 -0
  66. package/dist/utils/language.d.ts.map +1 -0
  67. package/dist/utils/language.js +44 -0
  68. package/dist/utils/language.js.map +1 -0
  69. package/dist/utils/scope.d.ts +8 -0
  70. package/dist/utils/scope.d.ts.map +1 -0
  71. package/dist/utils/scope.js +30 -0
  72. package/dist/utils/scope.js.map +1 -0
  73. package/dist/utils/sloop.d.ts +10 -0
  74. package/dist/utils/sloop.d.ts.map +1 -0
  75. package/dist/utils/sloop.js +39 -0
  76. package/dist/utils/sloop.js.map +1 -0
  77. package/dist/utils/transforms.d.ts +23 -0
  78. package/dist/utils/transforms.d.ts.map +1 -0
  79. package/dist/utils/transforms.js +64 -0
  80. package/dist/utils/transforms.js.map +1 -0
  81. package/package.json +10 -7
  82. package/scripts/setup-sonarlint.sh +115 -39
  83. package/dist/sonarlint-bridge.d.ts +0 -33
  84. package/dist/sonarlint-bridge.d.ts.map +0 -1
  85. package/dist/sonarlint-bridge.js +0 -91
  86. 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 { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
- import { SloopBridge } from "./sloop-bridge.js";
6
- import { existsSync, statSync, writeFileSync, unlinkSync, readdirSync, readFileSync } from "fs";
7
- import { join, dirname, extname, basename, relative } from "path";
8
- import { createHash } from "crypto";
9
- import { fileURLToPath } from "url";
10
- // Get package root directory (where sonarlint-backend is installed)
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = dirname(__filename);
13
- const PACKAGE_ROOT = join(__dirname, '..'); // Go up from dist/ to package root
14
- // Custom error class for better error handling
15
- class SloopError extends Error {
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 Server({
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
- // Helper: Ensure SLOOP bridge is initialized
45
- async function ensureSloopBridge() {
46
- if (!sloopBridge) {
47
- console.error("[MCP] Initializing SLOOP bridge...");
48
- // Check if plugins are downloaded
49
- const pluginsDir = join(PACKAGE_ROOT, "sonarlint-backend", "plugins");
50
- if (!existsSync(pluginsDir)) {
51
- throw new SloopError("Backend not found", "SonarLint backend not installed. The postinstall script may have failed. Try reinstalling: npm install -g @nielspeter/sonarlint-mcp-server", false);
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
- const bridge = await ensureSloopBridge();
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 (err) {
178
- console.error(`[FS] Failed to notify file system update for ${filePath}:`, err);
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
- return output;
323
- }
324
- // Tool definitions
325
- const tools = [
326
- {
327
- name: "analyze_file",
328
- 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.",
329
- inputSchema: {
330
- type: "object",
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
- // Register tools
497
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
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
- // Add batch results
514
- for (const [batchId, result] of batchResults) {
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
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
525
- const uri = request.params.uri;
526
- if (uri.startsWith('sonarlint://session/')) {
527
- const resourceId = uri.replace('sonarlint://session/', '');
528
- // Find matching result
529
- for (const [filePath, result] of sessionResults) {
530
- const fileResourceId = createHash('md5').update(filePath).digest('hex').substring(0, 8);
531
- if (fileResourceId === resourceId) {
532
- return {
533
- contents: [{
534
- uri,
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
- if (uri.startsWith('sonarlint://batch/')) {
543
- const batchId = uri.replace('sonarlint://batch/', '');
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
- // Tool handler functions (extracted to reduce cognitive complexity)
558
- async function handleAnalyzeFile(args) {
559
- const { filePath, minSeverity, excludeRules } = args;
560
- // Validate file exists
561
- if (!existsSync(filePath)) {
562
- throw new SloopError(`File not found: ${filePath}`, `The file ${filePath} does not exist. Please check the path and try again.`, false);
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
- // Ensure temp directory exists
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
- return {
924
- content: [
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
- // Write the modified content back
1115
- fileContent = lines.join('\n');
1116
- console.error(`[DEBUG] About to write file: ${filePath}`);
1117
- console.error(`[DEBUG] File content length: ${fileContent.length} chars`);
1118
- console.error(`[DEBUG] First 200 chars: ${fileContent.substring(0, 200)}`);
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
- writeFileSync(filePath, fileContent, 'utf-8');
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
- // Notify SLOOP that file system was updated (proper cache invalidation)
1128
- console.error(`[Cache] Sending file system update notification...`);
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
- summary += `Remaining issues: ${remainingIssues.length}\n\n`;
1258
- if (appliedFixes.length > 0) {
1259
- summary += `**Fixed Issues:**\n`;
1260
- for (const fix of appliedFixes) {
1261
- summary += `- Line ${fix.line}: ${fix.rule} - ${fix.message}\n`;
1262
- }
1263
- summary += `\n`;
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
- if (failedFixes.length > 0) {
1266
- summary += `**Failed Fixes:**\n`;
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
- if (remainingIssues.length > 0) {
1273
- summary += `**Remaining Issues (require manual fixing):**\n`;
1274
- const groupedBySeverity = transformedRemaining.reduce((acc, issue) => {
1275
- if (!acc[issue.severity])
1276
- acc[issue.severity] = [];
1277
- acc[issue.severity].push(issue);
1278
- return acc;
1279
- }, {});
1280
- for (const severity of ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']) {
1281
- const issues = groupedBySeverity[severity] || [];
1282
- if (issues.length > 0) {
1283
- summary += `\n${severity} (${issues.length}):\n`;
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
- else {
1291
- summary += `🎉 All issues resolved! The file has no remaining code quality issues.\n`;
126
+ catch (error) {
127
+ return handleToolError(error);
1292
128
  }
1293
- return {
1294
- content: [
1295
- {
1296
- type: "text",
1297
- text: summary,
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
- switch (name) {
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
- console.error("[MCP] Error handling tool call:", error);
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...");