@miocid152/sonarqube-mcp-server 1.0.0
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/PUBLISH.md +49 -0
- package/README.md +82 -0
- package/bin/sonarqube-mcp.js +19 -0
- package/index.js +327 -0
- package/package.json +24 -0
package/PUBLISH.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Guía para Publicar en npm
|
|
2
|
+
|
|
3
|
+
## 1. Iniciar sesión en npm
|
|
4
|
+
```bash
|
|
5
|
+
npm login
|
|
6
|
+
```
|
|
7
|
+
Te pedirá:
|
|
8
|
+
- Username
|
|
9
|
+
- Password
|
|
10
|
+
- Email
|
|
11
|
+
- OTP (si tienes 2FA activado)
|
|
12
|
+
|
|
13
|
+
## 2. Verificar que estás logueado
|
|
14
|
+
```bash
|
|
15
|
+
npm whoami
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 3. Verificar el package.json
|
|
19
|
+
```bash
|
|
20
|
+
cat package.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 4. Publicar el paquete
|
|
24
|
+
```bash
|
|
25
|
+
cd /Volumes/erick/Erick/proyectos/ProyectosEKT/sonarqube-mcp-server
|
|
26
|
+
npm publish
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 5. Verificar la publicación
|
|
30
|
+
```bash
|
|
31
|
+
npm view sonarqube-mcp-server
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 6. Instalar globalmente desde npm
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g sonarqube-mcp-server
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Notas importantes:
|
|
40
|
+
- El nombre "sonarqube-mcp-server" debe estar disponible en npm
|
|
41
|
+
- Si ya existe, deberás cambiar el nombre en package.json
|
|
42
|
+
- La versión actual es 1.0.0
|
|
43
|
+
- Para actualizar: cambia la versión en package.json y ejecuta `npm publish` de nuevo
|
|
44
|
+
|
|
45
|
+
## Si el nombre está ocupado:
|
|
46
|
+
Cambia el nombre en package.json a algo como:
|
|
47
|
+
- @tu-usuario/sonarqube-mcp-server (scoped package)
|
|
48
|
+
- sonarqube-mcp-server-ekt
|
|
49
|
+
- mcp-sonarqube-server
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# SonarQube MCP Server
|
|
2
|
+
|
|
3
|
+
Servidor MCP (Model Context Protocol) para integración con SonarQube que permite obtener reglas de calidad y generar código conforme.
|
|
4
|
+
|
|
5
|
+
## Instalación
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Instalar globalmente
|
|
9
|
+
npm install -g sonarqube-mcp-server
|
|
10
|
+
|
|
11
|
+
# O instalar desde el directorio local
|
|
12
|
+
cd sonarqube-mcp-server
|
|
13
|
+
npm install -g .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configuración
|
|
17
|
+
|
|
18
|
+
Configurar variables de entorno:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export SONAR_URL="https://sonar.tu-empresa.com"
|
|
22
|
+
export SONAR_TOKEN="tu_token_de_sonar"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Uso
|
|
26
|
+
|
|
27
|
+
### Ejecutar el servidor MCP
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
sonarqube-mcp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Herramientas disponibles
|
|
34
|
+
|
|
35
|
+
1. **get_sonar_rules** - Obtener reglas activas
|
|
36
|
+
- `language`: Lenguaje de programación (java, javascript, python, etc.)
|
|
37
|
+
- `projectKey`: Clave del proyecto (opcional)
|
|
38
|
+
|
|
39
|
+
2. **get_quality_profile** - Obtener perfil de calidad
|
|
40
|
+
- `projectKey`: Clave del proyecto
|
|
41
|
+
|
|
42
|
+
3. **get_project_issues** - Obtener issues del proyecto
|
|
43
|
+
- `projectKey`: Clave del proyecto
|
|
44
|
+
- `resolved`: Incluir issues resueltos (default: false)
|
|
45
|
+
|
|
46
|
+
4. **validate_code_rules** - Obtener reglas para validación
|
|
47
|
+
- `language`: Lenguaje de programación
|
|
48
|
+
- `severity`: Severidad mínima (INFO, MINOR, MAJOR, CRITICAL, BLOCKER)
|
|
49
|
+
|
|
50
|
+
## Ejemplo de configuración en Claude Desktop
|
|
51
|
+
|
|
52
|
+
Agregar al archivo de configuración de Claude Desktop:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"sonarqube": {
|
|
58
|
+
"command": "sonarqube-mcp",
|
|
59
|
+
"env": {
|
|
60
|
+
"SONAR_URL": "https://sonar.tu-empresa.com",
|
|
61
|
+
"SONAR_TOKEN": "tu_token_aqui"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Obtener Token de SonarQube
|
|
69
|
+
|
|
70
|
+
1. Ir a SonarQube → User → My Account → Security
|
|
71
|
+
2. Generar nuevo token
|
|
72
|
+
3. Copiar el token generado
|
|
73
|
+
|
|
74
|
+
## Desarrollo
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Instalar dependencias
|
|
78
|
+
npm install
|
|
79
|
+
|
|
80
|
+
# Ejecutar en modo desarrollo
|
|
81
|
+
npm start
|
|
82
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { SonarQubeMCPServer } = require('../index.js');
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const sonarUrl = process.env.SONAR_URL;
|
|
7
|
+
const sonarToken = process.env.SONAR_TOKEN;
|
|
8
|
+
|
|
9
|
+
if (!sonarUrl || !sonarToken) {
|
|
10
|
+
console.error('Error: SONAR_URL and SONAR_TOKEN environment variables are required');
|
|
11
|
+
console.error('Usage: SONAR_URL=https://sonar.company.com SONAR_TOKEN=your_token sonarqube-mcp');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const server = new SonarQubeMCPServer(sonarUrl, sonarToken);
|
|
16
|
+
await server.start();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
main().catch(console.error);
|
package/index.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
2
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
3
|
+
const {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
7
|
+
|
|
8
|
+
class SonarQubeMCPServer {
|
|
9
|
+
constructor(sonarUrl, sonarToken) {
|
|
10
|
+
this.sonarUrl = sonarUrl;
|
|
11
|
+
this.sonarToken = sonarToken;
|
|
12
|
+
this.server = new Server(
|
|
13
|
+
{
|
|
14
|
+
name: 'sonarqube-mcp-server',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
capabilities: {
|
|
19
|
+
tools: {},
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
this.setupHandlers();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setupHandlers() {
|
|
28
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
29
|
+
tools: [
|
|
30
|
+
{
|
|
31
|
+
name: 'get_sonar_rules',
|
|
32
|
+
description: 'Get SonarQube rules with pagination. Use multiple calls with different page numbers to get all rules and avoid character limits. Returns page info to guide next calls.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
language: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Programming language (java, javascript, python, etc.)',
|
|
39
|
+
default: 'java'
|
|
40
|
+
},
|
|
41
|
+
page: {
|
|
42
|
+
type: 'number',
|
|
43
|
+
description: 'Page number (1-based)',
|
|
44
|
+
default: 1
|
|
45
|
+
},
|
|
46
|
+
pageSize: {
|
|
47
|
+
type: 'number',
|
|
48
|
+
description: 'Number of rules per page (max 100)',
|
|
49
|
+
default: 50
|
|
50
|
+
},
|
|
51
|
+
projectKey: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'SonarQube project key (optional)'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
required: ['language']
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'get_quality_profile',
|
|
61
|
+
description: 'Get quality profile for a project',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
projectKey: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'SonarQube project key'
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
required: ['projectKey']
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'get_project_issues',
|
|
75
|
+
description: 'Get issues for a project',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
projectKey: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'SonarQube project key'
|
|
82
|
+
},
|
|
83
|
+
resolved: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
description: 'Include resolved issues',
|
|
86
|
+
default: false
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
required: ['projectKey']
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'validate_code_rules',
|
|
94
|
+
description: 'Get rule violations for code validation',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
language: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Programming language',
|
|
101
|
+
default: 'java'
|
|
102
|
+
},
|
|
103
|
+
severity: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Minimum severity (INFO, MINOR, MAJOR, CRITICAL, BLOCKER)',
|
|
106
|
+
default: 'MAJOR'
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
required: ['language']
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
116
|
+
const { name, arguments: args } = request.params;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
switch (name) {
|
|
120
|
+
case 'get_sonar_rules':
|
|
121
|
+
return await this.getSonarRulesPage(args.language, args.page || 1, args.pageSize || 50, args.projectKey);
|
|
122
|
+
|
|
123
|
+
case 'get_quality_profile':
|
|
124
|
+
return await this.getQualityProfile(args.projectKey);
|
|
125
|
+
|
|
126
|
+
case 'get_project_issues':
|
|
127
|
+
return await this.getProjectIssues(args.projectKey, args.resolved);
|
|
128
|
+
|
|
129
|
+
case 'validate_code_rules':
|
|
130
|
+
return await this.getValidationRules(args.language, args.severity);
|
|
131
|
+
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: `Error: ${error.message}`
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async makeRequest(endpoint, params = {}) {
|
|
149
|
+
const url = new URL(`${this.sonarUrl}/api/${endpoint}`);
|
|
150
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
151
|
+
if (value !== undefined) url.searchParams.append(key, value);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
console.error(`Making request to: ${url.toString()}`);
|
|
155
|
+
|
|
156
|
+
const response = await fetch(url, {
|
|
157
|
+
headers: {
|
|
158
|
+
'Authorization': `Bearer ${this.sonarToken}`,
|
|
159
|
+
'Accept': 'application/json'
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error(`SonarQube API error: ${response.status} ${response.statusText}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return await response.json();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async getQualityProfile(projectKey) {
|
|
171
|
+
const profile = await this.makeRequest('qualityprofiles/search', { project: projectKey });
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: 'text',
|
|
177
|
+
text: JSON.stringify(profile, null, 2)
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async getSonarRulesPage(language, page = 1, pageSize = 50, projectKey) {
|
|
184
|
+
const languageMap = {
|
|
185
|
+
'python': 'py',
|
|
186
|
+
'javascript': 'js',
|
|
187
|
+
'typescript': 'ts',
|
|
188
|
+
'java': 'java',
|
|
189
|
+
'csharp': 'cs',
|
|
190
|
+
'cpp': 'cpp',
|
|
191
|
+
'c': 'c'
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const sonarLanguage = languageMap[language] || language;
|
|
195
|
+
const actualPageSize = Math.min(pageSize, 100);
|
|
196
|
+
|
|
197
|
+
const params = {
|
|
198
|
+
activation: 'true',
|
|
199
|
+
languages: sonarLanguage,
|
|
200
|
+
ps: actualPageSize,
|
|
201
|
+
p: page
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (projectKey) {
|
|
205
|
+
const profile = await this.makeRequest('qualityprofiles/search', { project: projectKey });
|
|
206
|
+
if (profile.profiles?.length > 0) {
|
|
207
|
+
const langProfile = profile.profiles.find(p => p.language === sonarLanguage);
|
|
208
|
+
if (langProfile) {
|
|
209
|
+
params.qprofile = langProfile.key;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const rules = await this.makeRequest('rules/search', params);
|
|
215
|
+
const totalPages = Math.ceil(rules.total / actualPageSize);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
content: [
|
|
219
|
+
{
|
|
220
|
+
type: 'text',
|
|
221
|
+
text: JSON.stringify({
|
|
222
|
+
total: rules.total,
|
|
223
|
+
language: language,
|
|
224
|
+
projectKey: projectKey,
|
|
225
|
+
page: page,
|
|
226
|
+
pageSize: actualPageSize,
|
|
227
|
+
totalPages: totalPages,
|
|
228
|
+
hasMore: page < totalPages,
|
|
229
|
+
rules: rules.rules.map(rule => ({
|
|
230
|
+
key: rule.key,
|
|
231
|
+
name: rule.name,
|
|
232
|
+
severity: rule.severity,
|
|
233
|
+
type: rule.type,
|
|
234
|
+
tags: rule.tags
|
|
235
|
+
}))
|
|
236
|
+
}, null, 2)
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async getProjectIssues(projectKey, resolved = false) {
|
|
243
|
+
const issues = await this.makeRequest('issues/search', {
|
|
244
|
+
componentKeys: projectKey,
|
|
245
|
+
resolved: resolved.toString(),
|
|
246
|
+
ps: 100
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{
|
|
252
|
+
type: 'text',
|
|
253
|
+
text: JSON.stringify({
|
|
254
|
+
total: issues.total,
|
|
255
|
+
issues: issues.issues.map(issue => ({
|
|
256
|
+
key: issue.key,
|
|
257
|
+
rule: issue.rule,
|
|
258
|
+
severity: issue.severity,
|
|
259
|
+
component: issue.component,
|
|
260
|
+
line: issue.line,
|
|
261
|
+
message: issue.message,
|
|
262
|
+
status: issue.status,
|
|
263
|
+
type: issue.type
|
|
264
|
+
}))
|
|
265
|
+
}, null, 2)
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async getValidationRules(language, severity = 'MAJOR') {
|
|
272
|
+
const severityLevels = ['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'];
|
|
273
|
+
const minSeverityIndex = severityLevels.indexOf(severity);
|
|
274
|
+
|
|
275
|
+
// Mapear lenguajes a códigos de SonarQube
|
|
276
|
+
const languageMap = {
|
|
277
|
+
'python': 'py',
|
|
278
|
+
'javascript': 'js',
|
|
279
|
+
'typescript': 'ts',
|
|
280
|
+
'java': 'java',
|
|
281
|
+
'csharp': 'cs',
|
|
282
|
+
'cpp': 'cpp',
|
|
283
|
+
'c': 'c'
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const sonarLanguage = languageMap[language] || language;
|
|
287
|
+
|
|
288
|
+
const rules = await this.makeRequest('rules/search', {
|
|
289
|
+
activation: 'true',
|
|
290
|
+
languages: sonarLanguage,
|
|
291
|
+
ps: 500
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const filteredRules = rules.rules
|
|
295
|
+
.filter(rule => severityLevels.indexOf(rule.severity) >= minSeverityIndex)
|
|
296
|
+
.map(rule => ({
|
|
297
|
+
key: rule.key,
|
|
298
|
+
name: rule.name,
|
|
299
|
+
severity: rule.severity,
|
|
300
|
+
type: rule.type,
|
|
301
|
+
description: rule.htmlDesc || rule.mdDesc,
|
|
302
|
+
tags: rule.tags
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
type: 'text',
|
|
309
|
+
text: JSON.stringify({
|
|
310
|
+
language: language,
|
|
311
|
+
minimumSeverity: severity,
|
|
312
|
+
totalRules: filteredRules.length,
|
|
313
|
+
rules: filteredRules
|
|
314
|
+
}, null, 2)
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async start() {
|
|
321
|
+
const transport = new StdioServerTransport();
|
|
322
|
+
await this.server.connect(transport);
|
|
323
|
+
console.error('SonarQube MCP Server started');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = { SonarQubeMCPServer };
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@miocid152/sonarqube-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP Server for SonarQube integration",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sonarqube-mcp": "./bin/sonarqube-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node index.js",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["mcp", "sonarqube", "code-quality"],
|
|
14
|
+
"author": "Your Name",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^0.4.0",
|
|
18
|
+
"node-fetch": "^3.3.2"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"preferGlobal": true
|
|
24
|
+
}
|