Skip to content
This repository was archived by the owner on Nov 14, 2025. It is now read-only.

Commit 99159a6

Browse files
sapientpantsclaude
andcommitted
Add system API support for health, status, and ping endpoints
This implementation adds support for the following GET endpoints: - /api/system/health - Returns health status of the SonarQube instance - /api/system/status - Returns system status including version and state - /api/system/ping - Simple ping endpoint for availability checks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 079a401 commit 99159a6

File tree

5 files changed

+325
-2
lines changed

5 files changed

+325
-2
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ pnpm-debug.log*
2424

2525
# OS files
2626
.DS_Store
27-
Thumbs.db
27+
Thumbs.db
28+
**/.claude/settings.local.json

src/__tests__/index.test.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ beforeAll(() => {
8686
total: 1,
8787
},
8888
});
89+
90+
nock('http://localhost:9000').persist().get('/api/system/health').reply(200, {
91+
health: 'GREEN',
92+
causes: [],
93+
});
94+
95+
nock('http://localhost:9000').persist().get('/api/system/status').reply(200, {
96+
id: 'test-id',
97+
version: '10.3.0.82913',
98+
status: 'UP',
99+
});
100+
101+
nock('http://localhost:9000').persist().get('/api/system/ping').reply(200, 'pong');
89102
});
90103

91104
afterAll(() => {
@@ -172,6 +185,37 @@ const mockHandlers = {
172185
},
173186
],
174187
}),
188+
handleSonarQubeGetHealth: jest.fn().mockResolvedValue({
189+
content: [
190+
{
191+
type: 'text' as const,
192+
text: JSON.stringify({
193+
health: 'GREEN',
194+
causes: [],
195+
}),
196+
},
197+
],
198+
}),
199+
handleSonarQubeGetStatus: jest.fn().mockResolvedValue({
200+
content: [
201+
{
202+
type: 'text' as const,
203+
text: JSON.stringify({
204+
id: 'test-id',
205+
version: '10.3.0.82913',
206+
status: 'UP',
207+
}),
208+
},
209+
],
210+
}),
211+
handleSonarQubePing: jest.fn().mockResolvedValue({
212+
content: [
213+
{
214+
type: 'text' as const,
215+
text: 'pong',
216+
},
217+
],
218+
}),
175219
};
176220

177221
// Define the mock handlers but don't mock the entire module
@@ -199,6 +243,9 @@ let handleSonarQubeProjects: any;
199243
let mapToSonarQubeParams: any;
200244
let handleSonarQubeGetIssues: any;
201245
let handleSonarQubeGetMetrics: any;
246+
let handleSonarQubeGetHealth: any;
247+
let handleSonarQubeGetStatus: any;
248+
let handleSonarQubePing: any;
202249
/* eslint-enable @typescript-eslint/no-explicit-any */
203250

204251
interface Connectable {
@@ -214,6 +261,9 @@ describe('MCP Server', () => {
214261
mapToSonarQubeParams = module.mapToSonarQubeParams;
215262
handleSonarQubeGetIssues = module.handleSonarQubeGetIssues;
216263
handleSonarQubeGetMetrics = module.handleSonarQubeGetMetrics;
264+
handleSonarQubeGetHealth = module.handleSonarQubeGetHealth;
265+
handleSonarQubeGetStatus = module.handleSonarQubeGetStatus;
266+
handleSonarQubePing = module.handleSonarQubePing;
217267
});
218268

219269
beforeEach(() => {
@@ -280,13 +330,37 @@ describe('MCP Server', () => {
280330
},
281331
mockHandlers.handleSonarQubeGetIssues
282332
);
333+
334+
testServer.tool(
335+
'system_health',
336+
'Get the health status of the SonarQube instance',
337+
{},
338+
mockHandlers.handleSonarQubeGetHealth
339+
);
340+
341+
testServer.tool(
342+
'system_status',
343+
'Get the status of the SonarQube instance',
344+
{},
345+
mockHandlers.handleSonarQubeGetStatus
346+
);
347+
348+
testServer.tool(
349+
'system_ping',
350+
'Ping the SonarQube instance to check if it is up',
351+
{},
352+
mockHandlers.handleSonarQubePing
353+
);
283354
});
284355

285356
it('should register all required tools', () => {
286-
expect(registeredTools.size).toBe(3);
357+
expect(registeredTools.size).toBe(6);
287358
expect(registeredTools.has('projects')).toBe(true);
288359
expect(registeredTools.has('metrics')).toBe(true);
289360
expect(registeredTools.has('issues')).toBe(true);
361+
expect(registeredTools.has('system_health')).toBe(true);
362+
expect(registeredTools.has('system_status')).toBe(true);
363+
expect(registeredTools.has('system_ping')).toBe(true);
290364
});
291365

292366
it('should register tools with correct descriptions', () => {
@@ -295,12 +369,28 @@ describe('MCP Server', () => {
295369
'Get available metrics from SonarQube'
296370
);
297371
expect(registeredTools.get('issues').description).toBe('Get issues for a SonarQube project');
372+
expect(registeredTools.get('system_health').description).toBe(
373+
'Get the health status of the SonarQube instance'
374+
);
375+
expect(registeredTools.get('system_status').description).toBe(
376+
'Get the status of the SonarQube instance'
377+
);
378+
expect(registeredTools.get('system_ping').description).toBe(
379+
'Ping the SonarQube instance to check if it is up'
380+
);
298381
});
299382

300383
it('should register tools with correct handlers', () => {
301384
expect(registeredTools.get('projects').handler).toBe(mockHandlers.handleSonarQubeProjects);
302385
expect(registeredTools.get('metrics').handler).toBe(mockHandlers.handleSonarQubeGetMetrics);
303386
expect(registeredTools.get('issues').handler).toBe(mockHandlers.handleSonarQubeGetIssues);
387+
expect(registeredTools.get('system_health').handler).toBe(
388+
mockHandlers.handleSonarQubeGetHealth
389+
);
390+
expect(registeredTools.get('system_status').handler).toBe(
391+
mockHandlers.handleSonarQubeGetStatus
392+
);
393+
expect(registeredTools.get('system_ping').handler).toBe(mockHandlers.handleSonarQubePing);
304394
});
305395
});
306396

@@ -398,6 +488,40 @@ describe('MCP Server', () => {
398488
});
399489
});
400490

491+
describe('handleSonarQubeGetHealth', () => {
492+
it('should fetch and return health status', async () => {
493+
nock('http://localhost:9000').get('/api/system/health').reply(200, {
494+
health: 'GREEN',
495+
causes: [],
496+
});
497+
498+
const response = await handleSonarQubeGetHealth();
499+
expect(response.content[0].text).toContain('GREEN');
500+
});
501+
});
502+
503+
describe('handleSonarQubeGetStatus', () => {
504+
it('should fetch and return system status', async () => {
505+
nock('http://localhost:9000').get('/api/system/status').reply(200, {
506+
id: 'test-id',
507+
version: '10.3.0.82913',
508+
status: 'UP',
509+
});
510+
511+
const response = await handleSonarQubeGetStatus();
512+
expect(response.content[0].text).toContain('UP');
513+
});
514+
});
515+
516+
describe('handleSonarQubePing', () => {
517+
it('should ping the system and return the result', async () => {
518+
nock('http://localhost:9000').get('/api/system/ping').reply(200, 'pong');
519+
520+
const response = await handleSonarQubePing();
521+
expect(response.content[0].text).toBe('pong');
522+
});
523+
});
524+
401525
describe('Conditional server start', () => {
402526
it('should not start the server if NODE_ENV is test', async () => {
403527
process.env.NODE_ENV = 'test';

src/__tests__/sonarqube.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,4 +451,70 @@ describe('SonarQubeClient', () => {
451451
expect(scope.isDone()).toBe(true);
452452
});
453453
});
454+
455+
describe('getHealth', () => {
456+
it('should fetch health status successfully', async () => {
457+
const mockResponse = {
458+
health: 'GREEN',
459+
causes: [],
460+
};
461+
462+
nock(baseUrl)
463+
.get('/api/system/health')
464+
.basicAuth({ user: token, pass: '' })
465+
.reply(200, mockResponse);
466+
467+
const result = await client.getHealth();
468+
expect(result).toEqual(mockResponse);
469+
expect(result.health).toBe('GREEN');
470+
expect(result.causes).toEqual([]);
471+
});
472+
473+
it('should handle warning health status', async () => {
474+
const mockResponse = {
475+
health: 'YELLOW',
476+
causes: ['Disk space low'],
477+
};
478+
479+
nock(baseUrl)
480+
.get('/api/system/health')
481+
.basicAuth({ user: token, pass: '' })
482+
.reply(200, mockResponse);
483+
484+
const result = await client.getHealth();
485+
expect(result).toEqual(mockResponse);
486+
expect(result.health).toBe('YELLOW');
487+
expect(result.causes).toContain('Disk space low');
488+
});
489+
});
490+
491+
describe('getStatus', () => {
492+
it('should fetch system status successfully', async () => {
493+
const mockResponse = {
494+
id: '20230101-1234',
495+
version: '10.3.0.82913',
496+
status: 'UP',
497+
};
498+
499+
nock(baseUrl)
500+
.get('/api/system/status')
501+
.basicAuth({ user: token, pass: '' })
502+
.reply(200, mockResponse);
503+
504+
const result = await client.getStatus();
505+
expect(result).toEqual(mockResponse);
506+
expect(result.id).toBe('20230101-1234');
507+
expect(result.version).toBe('10.3.0.82913');
508+
expect(result.status).toBe('UP');
509+
});
510+
});
511+
512+
describe('ping', () => {
513+
it('should ping SonarQube successfully', async () => {
514+
nock(baseUrl).get('/api/system/ping').basicAuth({ user: token, pass: '' }).reply(200, 'pong');
515+
516+
const result = await client.ping();
517+
expect(result).toBe('pong');
518+
});
519+
});
454520
});

src/index.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,57 @@ export async function handleSonarQubeGetMetrics(params: PaginationParams) {
193193
};
194194
}
195195

196+
/**
197+
* Handler for getting SonarQube system health status
198+
* @returns Promise with the health status result
199+
*/
200+
export async function handleSonarQubeGetHealth() {
201+
const result = await client.getHealth();
202+
203+
return {
204+
content: [
205+
{
206+
type: 'text' as const,
207+
text: JSON.stringify(result),
208+
},
209+
],
210+
};
211+
}
212+
213+
/**
214+
* Handler for getting SonarQube system status
215+
* @returns Promise with the system status result
216+
*/
217+
export async function handleSonarQubeGetStatus() {
218+
const result = await client.getStatus();
219+
220+
return {
221+
content: [
222+
{
223+
type: 'text' as const,
224+
text: JSON.stringify(result),
225+
},
226+
],
227+
};
228+
}
229+
230+
/**
231+
* Handler for pinging SonarQube system
232+
* @returns Promise with the ping result
233+
*/
234+
export async function handleSonarQubePing() {
235+
const result = await client.ping();
236+
237+
return {
238+
content: [
239+
{
240+
type: 'text' as const,
241+
text: result,
242+
},
243+
],
244+
};
245+
}
246+
196247
// Define SonarQube severity schema for validation
197248
const severitySchema = z
198249
.enum(['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'])
@@ -322,6 +373,28 @@ mcpServer.tool(
322373
}
323374
);
324375

376+
// Register system API tools
377+
mcpServer.tool(
378+
'system_health',
379+
'Get the health status of the SonarQube instance',
380+
{},
381+
handleSonarQubeGetHealth
382+
);
383+
384+
mcpServer.tool(
385+
'system_status',
386+
'Get the status of the SonarQube instance',
387+
{},
388+
handleSonarQubeGetStatus
389+
);
390+
391+
mcpServer.tool(
392+
'system_ping',
393+
'Ping the SonarQube instance to check if it is up',
394+
{},
395+
handleSonarQubePing
396+
);
397+
325398
// Only start the server if not in test mode
326399
/* istanbul ignore if */
327400
if (process.env.NODE_ENV !== 'test') {

0 commit comments

Comments
 (0)