Skip to main content
Glama

MCP-Jenkins

by gcorroto
index.ts15.9 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Import Jenkins service import { JenkinsService } from "./tools/jenkins-service.js"; import { formatDuration, formatTimestamp } from "./common/utils.js"; import { CoverageSummary } from "./common/types.js"; import { VERSION } from "./common/version.js"; // Create the MCP Server with proper configuration const server = new McpServer({ name: "jenkins-mcp-server", version: VERSION, }); // Create Jenkins service instance (lazy initialization) let jenkinsService: JenkinsService | null = null; function getJenkinsService(): JenkinsService { if (!jenkinsService) { try { jenkinsService = new JenkinsService(); } catch (error: any) { throw new Error(`Jenkins configuration error: ${error.message}`); } } return jenkinsService; } // ----- HERRAMIENTAS MCP PARA JENKINS CI/CD ----- // 1. Obtener estado de un job server.tool( "jenkins_get_job_status", "Obtener el estado de un job específico de Jenkins", { app: z.string().describe("Nombre de la aplicación"), branch: z.string().optional().describe("Rama de Git (por defecto: main)") }, async (args) => { try { const result = await getJenkinsService().getJobStatus(args.app, args.branch || 'main'); const lastBuild = result.lastBuild; const statusText = `🔧 **Estado del Job: ${result.displayName}**\n\n` + `**URL:** ${result.url}\n` + `**Estado:** ${result.color}\n` + `**Construible:** ${result.buildable ? '✅' : '❌'}\n` + `**Próximo build:** #${result.nextBuildNumber}\n\n` + (lastBuild ? `**Último build:** #${lastBuild.number}\n` + `**Resultado:** ${lastBuild.result || 'En progreso'}\n` + `**Duración:** ${formatDuration(lastBuild.duration || 0)}\n` + `**Timestamp:** ${formatTimestamp(lastBuild.timestamp || 0)}` : 'Sin builds previos'); return { content: [{ type: "text", text: statusText }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 2. Iniciar un job server.tool( "jenkins_start_job", "Iniciar un job de Jenkins con una rama específica", { app: z.string().describe("Nombre de la aplicación"), branch: z.string().describe("Rama de Git a construir") }, async (args) => { try { const result = await getJenkinsService().startJob(args.app, args.branch); return { content: [{ type: "text", text: `🚀 **${result}**` }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 3. Detener un job server.tool( "jenkins_stop_job", "Detener un job de Jenkins en ejecución", { app: z.string().describe("Nombre de la aplicación"), buildNumber: z.number().describe("Número del build a detener"), branch: z.string().optional().describe("Rama de Git (por defecto: main)") }, async (args) => { try { const result = await getJenkinsService().stopJob(args.app, args.buildNumber, args.branch || 'main'); return { content: [{ type: "text", text: `🛑 **${result}**` }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 4. Obtener estado de los steps de un build server.tool( "jenkins_get_build_steps", "Obtener el estado de los steps de un build específico", { app: z.string().describe("Nombre de la aplicación"), buildNumber: z.number().describe("Número del build"), branch: z.string().optional().describe("Rama de Git (por defecto: main)") }, async (args) => { try { const result = await getJenkinsService().getJobStepsStatus(args.app, args.buildNumber, args.branch || 'main'); const stepsText = `📋 **Steps del Build #${args.buildNumber} - ${args.app}**\n\n` + `**ID:** ${result.id}\n` + `**Nombre:** ${result.name}\n` + `**Estado:** ${result.status}\n` + `**Duración:** ${formatDuration(result.durationMillis)}\n` + `**Inicio:** ${formatTimestamp(result.startTimeMillis)}\n\n` + `**Stages (${result.stages.length}):**\n` + result.stages.map(stage => `- **${stage.name}** (${stage.status}) - ${formatDuration(stage.durationMillis)}` ).join('\n'); return { content: [{ type: "text", text: stepsText }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 5. Obtener estado de un nodo específico server.tool( "jenkins_get_node_status", "Obtener el estado de un nodo específico de un build", { app: z.string().describe("Nombre de la aplicación"), buildNumber: z.number().describe("Número del build"), nodeId: z.string().describe("ID del nodo"), branch: z.string().optional().describe("Rama de Git (por defecto: main)") }, async (args) => { try { const result = await getJenkinsService().getNodeStatus(args.app, args.buildNumber, args.nodeId, args.branch || 'main'); let statusText: string; if ('proceedUrl' in result) { // Es un PendingInputAction statusText = `⏸️ **Nodo Esperando Input - ${args.nodeId}**\n\n` + `**ID:** ${result.id}\n` + `**Proceed URL:** ${result.proceedUrl}\n` + `**Abort URL:** ${result.abortUrl}\n` + (result.message ? `**Mensaje:** ${result.message}` : ''); } else { // Es un NodeStatus normal statusText = `🔍 **Estado del Nodo: ${args.nodeId}**\n\n` + `**Nombre:** ${result.name}\n` + `**Estado:** ${result.status}\n` + `**Duración:** ${formatDuration(result.durationMillis)}\n` + `**Inicio:** ${formatTimestamp(result.startTimeMillis)}`; } return { content: [{ type: "text", text: statusText }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 6. Obtener acciones pendientes de input server.tool( "jenkins_get_pending_actions", "Obtener las acciones pendientes de input de un build", { app: z.string().describe("Nombre de la aplicación"), buildNumber: z.number().describe("Número del build"), branch: z.string().optional().describe("Rama de Git (por defecto: main)") }, async (args) => { try { const result = await getJenkinsService().getPendingInputActions(args.app, args.buildNumber, args.branch || 'main'); const pendingText = `⏳ **Acciones Pendientes - Build #${args.buildNumber}**\n\n` + `**ID:** ${result.id}\n` + `**Proceed URL:** ${result.proceedUrl}\n` + `**Abort URL:** ${result.abortUrl}\n` + (result.message ? `**Mensaje:** ${result.message}\n` : '') + (result.inputs && result.inputs.length > 0 ? `**Inputs requeridos:**\n${result.inputs.map(input => `- ${input.name}: ${input.description || 'N/A'}`).join('\n')}` : ''); return { content: [{ type: "text", text: pendingText }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 7. Enviar acción de input server.tool( "jenkins_submit_input_action", "Enviar una acción de input a Jenkins (aprobar/rechazar)", { decisionUrl: z.string().describe("URL de la decisión (proceedUrl o abortUrl)") }, async (args) => { try { const result = await getJenkinsService().submitInputAction(args.decisionUrl); return { content: [{ type: "text", text: `✅ **${result}**` }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 8. Obtener reporte de cobertura server.tool( "jenkins_get_coverage_report", "Obtener reporte de cobertura de código de un build", { app: z.string().describe("Nombre de la aplicación"), buildNumber: z.number().describe("Número del build"), packageName: z.string().optional().describe("Nombre del paquete específico"), className: z.string().optional().describe("Nombre de la clase específica"), branch: z.string().optional().describe("Rama de Git (por defecto: main)") }, async (args) => { try { const result = await getJenkinsService().getCoverageReport( args.app, args.buildNumber, args.packageName, args.className, args.branch || 'main' ); let coverageText: string; if ('instructionCoverage' in result) { // Es un CoverageReport (backend) coverageText = `📊 **Reporte de Cobertura - Build #${args.buildNumber}**\n\n` + `**Instrucciones:** ${result.instructionCoverage?.percentage || 0}% (${result.instructionCoverage?.covered || 0}/${result.instructionCoverage?.total || 0})\n` + `**Ramas:** ${result.branchCoverage?.percentage || 0}% (${result.branchCoverage?.covered || 0}/${result.branchCoverage?.total || 0})\n` + `**Líneas:** ${result.lineCoverage?.percentage || 0}% (${result.lineCoverage?.covered || 0}/${result.lineCoverage?.total || 0})`; } else { // Es un CoverageSummary (frontend) const summary = result as CoverageSummary; const stmtPercent = summary.statements > 0 ? ((summary.statements - summary.uncoveredStatements) / summary.statements * 100).toFixed(2) : 0; const funcPercent = summary.functions > 0 ? ((summary.functions - summary.uncoveredFunctions) / summary.functions * 100).toFixed(2) : 0; const branchPercent = summary.branches > 0 ? ((summary.branches - summary.uncoveredBranches) / summary.branches * 100).toFixed(2) : 0; coverageText = `📊 **Resumen de Cobertura - Build #${args.buildNumber}**\n\n` + `**Declaraciones:** ${stmtPercent}% (${summary.statements - summary.uncoveredStatements}/${summary.statements})\n` + `**Funciones:** ${funcPercent}% (${summary.functions - summary.uncoveredFunctions}/${summary.functions})\n` + `**Ramas:** ${branchPercent}% (${summary.branches - summary.uncoveredBranches}/${summary.branches})`; } return { content: [{ type: "text", text: coverageText }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 9. Obtener líneas de cobertura por archivo server.tool( "jenkins_get_coverage_lines", "Obtener líneas de cobertura de un archivo específico", { app: z.string().describe("Nombre de la aplicación"), buildNumber: z.number().describe("Número del build"), path: z.string().describe("Ruta del archivo"), branch: z.string().optional().describe("Rama de Git (por defecto: main)") }, async (args) => { try { const result = await getJenkinsService().getCoverageReportLines(args.app, args.buildNumber, args.path, args.branch || 'main'); const linesText = `📄 **Cobertura de Archivo: ${args.path}**\n\n` + `**Declaraciones:** ${Object.keys(result.statementMap).length}\n` + `**Funciones:** ${Object.keys(result.fnMap).length}\n` + `**Ramas:** ${Object.keys(result.branchMap).length}\n\n` + `**Líneas cubiertas:** ${Object.values(result.s).filter(v => v > 0).length}/${Object.keys(result.s).length}\n` + `**Funciones cubiertas:** ${Object.values(result.f).filter(v => v > 0).length}/${Object.keys(result.f).length}`; return { content: [{ type: "text", text: linesText }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 10. Obtener todos los paths de cobertura server.tool( "jenkins_get_coverage_paths", "Obtener todos los paths de archivos con cobertura", { app: z.string().describe("Nombre de la aplicación"), buildNumber: z.number().describe("Número del build"), branch: z.string().optional().describe("Rama de Git (por defecto: main)") }, async (args) => { try { const result = await getJenkinsService().getCoverageReportPaths(args.app, args.buildNumber, args.branch || 'main'); const pathsText = `📂 **Paths de Cobertura - Build #${args.buildNumber}**\n\n` + `**Total de archivos:** ${result.length}\n\n` + result.slice(0, 20).map((path, index) => `${index + 1}. ${path}`).join('\n') + (result.length > 20 ? `\n\n... y ${result.length - 20} archivos más` : ''); return { content: [{ type: "text", text: pathsText }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); // 11. Obtener ramas de Git disponibles server.tool( "jenkins_get_git_branches", "Obtener las ramas de Git disponibles para un job", { app: z.string().describe("Nombre de la aplicación") }, async (args) => { try { const result = await getJenkinsService().getGitBranches(args.app); const branchesText = `🌿 **Ramas de Git Disponibles - ${args.app}**\n\n` + `**Total de ramas:** ${result.length}\n\n` + result.slice(0, 15).map((branch, index) => `${index + 1}. ${branch}`).join('\n') + (result.length > 15 ? `\n\n... y ${result.length - 15} ramas más` : ''); return { content: [{ type: "text", text: branchesText }], }; } catch (error: any) { return { content: [{ type: "text", text: `❌ **Error:** ${error.message}` }], }; } } ); async function runServer() { try { console.error("Creating Jenkins MCP Server..."); console.error("Server info: jenkins-mcp-server"); console.error("Version:", VERSION); // Validate environment variables if (!process.env.JENKINS_URL) { console.error("Warning: JENKINS_URL environment variable not set"); } else { console.error("JENKINS_URL:", process.env.JENKINS_URL); } if (!process.env.JENKINS_USERNAME) { console.error("Warning: JENKINS_USERNAME environment variable not set"); } else { console.error("JENKINS_USERNAME:", process.env.JENKINS_USERNAME); } if (!process.env.JENKINS_PASSWORD) { console.error("Warning: JENKINS_PASSWORD environment variable not set"); } else { console.error("JENKINS_PASSWORD:", "***"); } console.error("Starting Jenkins MCP Server in stdio mode..."); // Create transport const transport = new StdioServerTransport(); console.error("Connecting server to transport..."); // Connect server to transport - this should keep the process alive await server.connect(transport); console.error("MCP Server connected and ready!"); console.error("Available tools:", [ "jenkins_get_job_status", "jenkins_start_job", "jenkins_stop_job", "jenkins_get_build_steps", "jenkins_get_node_status", "jenkins_get_pending_actions", "jenkins_submit_input_action", "jenkins_get_coverage_report", "jenkins_get_coverage_lines", "jenkins_get_coverage_paths", "jenkins_get_git_branches" ]); } catch (error) { console.error("Error starting server:", error); console.error("Stack trace:", (error as Error).stack); process.exit(1); } } // Start the server runServer();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gcorroto/mcp-jenkins'

If you have feedback or need assistance with the MCP directory API, please join our Discord server