#!/usr/bin/env node
/**
* GitLab CI MCP Server
*
* Model Context Protocol server for GitLab CI/CD pipeline monitoring.
* Provides structured access to pipeline status, job logs, and failure analysis.
*/
import { config } from 'dotenv'
// Load .env from current working directory (the project Claude Code is running in)
config()
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js'
import {
listPipelines,
getPipeline,
listJobs,
getJobLogs,
getRecentFailures,
GITLAB_PROJECT,
type Pipeline,
type Job,
} from './lib/gitlab-api.js'
/**
* Tool definitions
*/
const TOOLS: Tool[] = [
{
name: 'gitlab_list_pipelines',
description: 'List recent GitLab CI/CD pipelines for the egirl-platform project',
inputSchema: {
type: 'object',
properties: {
count: {
type: 'number',
description: 'Number of pipelines to fetch (default: 10)',
default: 10,
},
},
},
},
{
name: 'gitlab_get_pipeline',
description: 'Get details for a specific pipeline by ID',
inputSchema: {
type: 'object',
properties: {
pipelineId: {
type: 'string',
description: 'The pipeline ID',
},
},
required: ['pipelineId'],
},
},
{
name: 'gitlab_list_jobs',
description: 'List all jobs in a specific pipeline',
inputSchema: {
type: 'object',
properties: {
pipelineId: {
type: 'string',
description: 'The pipeline ID',
},
},
required: ['pipelineId'],
},
},
{
name: 'gitlab_get_job_logs',
description: 'Get logs for a specific job in a pipeline',
inputSchema: {
type: 'object',
properties: {
pipelineId: {
type: 'string',
description: 'The pipeline ID',
},
jobNameOrId: {
type: 'string',
description: 'The job name or ID',
},
},
required: ['pipelineId', 'jobNameOrId'],
},
},
{
name: 'gitlab_get_recent_failures',
description: 'Get recent failed pipelines with detailed failure information',
inputSchema: {
type: 'object',
properties: {
count: {
type: 'number',
description: 'Number of failed pipelines to fetch (default: 3)',
default: 3,
},
},
},
},
{
name: 'gitlab_find_failure_cause',
description: 'Analyze a failed pipeline and extract likely root cause from logs',
inputSchema: {
type: 'object',
properties: {
pipelineId: {
type: 'string',
description: 'The pipeline ID to analyze',
},
},
required: ['pipelineId'],
},
},
]
/**
* Format pipeline for display
*/
function formatPipeline(pipeline: Pipeline): string {
return [
`Pipeline #${pipeline.id}`,
`Status: ${pipeline.status}`,
`Ref: ${pipeline.ref}`,
`SHA: ${pipeline.sha.substring(0, 8)}`,
`Updated: ${new Date(pipeline.updated_at).toLocaleString()}`,
`URL: ${pipeline.web_url}`,
].join('\n')
}
/**
* Format job for display
*/
function formatJob(job: Job): string {
return [
`Job #${job.id}: ${job.name}`,
`Status: ${job.status}`,
`Stage: ${job.stage}`,
`Duration: ${job.duration ? `${job.duration}s` : 'N/A'}`,
job.failure_reason ? `Failure: ${job.failure_reason}` : '',
]
.filter(Boolean)
.join('\n')
}
/**
* Strip ANSI color codes
*/
function stripAnsi(str: string): string {
return str.replace(/\x1b\[[0-9;]*m/g, '')
}
/**
* Extract error patterns from logs
*/
function extractErrorPatterns(logs: string): string[] {
const lines = logs.split('\n').map(stripAnsi)
const errors: string[] = []
// Look for common error patterns
const errorPatterns = [
/error:/i,
/failed/i,
/exception/i,
/cannot find/i,
/unexpected/i,
/syntax error/i,
/type error/i,
/reference error/i,
]
for (const line of lines) {
if (errorPatterns.some(pattern => pattern.test(line))) {
errors.push(line.trim())
}
}
return errors.slice(-10) // Last 10 error lines
}
/**
* Initialize MCP server
*/
async function main(): Promise<void> {
const server = new Server(
{
name: 'gitlab-ci-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
)
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}))
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
try {
switch (name) {
case 'gitlab_list_pipelines': {
const count = (args?.count as number) || 10
const pipelines = await listPipelines(count)
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
pipelines: pipelines.map(p => ({
id: p.id,
status: p.status,
ref: p.ref,
sha: p.sha.substring(0, 8),
updated_at: p.updated_at,
web_url: p.web_url,
})),
project_url: GITLAB_PROJECT.pipelinesUrl,
},
null,
2
),
},
],
}
}
case 'gitlab_get_pipeline': {
const pipelineId = args?.pipelineId as string
if (!pipelineId) {
throw new Error('pipelineId is required')
}
const pipeline = await getPipeline(pipelineId)
return {
content: [
{
type: 'text',
text: formatPipeline(pipeline),
},
],
}
}
case 'gitlab_list_jobs': {
const pipelineId = args?.pipelineId as string
if (!pipelineId) {
throw new Error('pipelineId is required')
}
const jobs = await listJobs(pipelineId)
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
pipeline_id: pipelineId,
jobs: jobs.map(j => ({
id: j.id,
name: j.name,
status: j.status,
stage: j.stage,
duration: j.duration,
failure_reason: j.failure_reason,
})),
},
null,
2
),
},
],
}
}
case 'gitlab_get_job_logs': {
const pipelineId = args?.pipelineId as string
const jobNameOrId = args?.jobNameOrId as string
if (!pipelineId || !jobNameOrId) {
throw new Error('pipelineId and jobNameOrId are required')
}
const { job, logs } = await getJobLogs(pipelineId, jobNameOrId)
return {
content: [
{
type: 'text',
text: [
formatJob(job),
'',
'--- LOGS ---',
logs,
'--- END LOGS ---',
].join('\n'),
},
],
}
}
case 'gitlab_get_recent_failures': {
const count = (args?.count as number) || 3
const failures = await getRecentFailures(count)
const summary = failures.map(({ pipeline, failedJobs }) => ({
pipeline: {
id: pipeline.id,
ref: pipeline.ref,
sha: pipeline.sha.substring(0, 8),
updated_at: pipeline.updated_at,
web_url: pipeline.web_url,
},
failed_jobs: failedJobs.map(j => ({
id: j.id,
name: j.name,
stage: j.stage,
failure_reason: j.failure_reason,
duration: j.duration,
})),
}))
return {
content: [
{
type: 'text',
text: JSON.stringify({ failures: summary }, null, 2),
},
],
}
}
case 'gitlab_find_failure_cause': {
const pipelineId = args?.pipelineId as string
if (!pipelineId) {
throw new Error('pipelineId is required')
}
const jobs = await listJobs(pipelineId)
const failedJobs = jobs.filter(j => j.status === 'failed')
if (failedJobs.length === 0) {
return {
content: [
{
type: 'text',
text: `Pipeline #${pipelineId} has no failed jobs`,
},
],
}
}
const analysis = []
for (const job of failedJobs) {
const { logs } = await getJobLogs(pipelineId, job.id)
const errors = extractErrorPatterns(logs)
analysis.push({
job: {
id: job.id,
name: job.name,
stage: job.stage,
failure_reason: job.failure_reason,
},
extracted_errors: errors,
})
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
pipeline_id: pipelineId,
analysis,
},
null,
2
),
},
],
}
}
default:
throw new Error(`Unknown tool: ${name}`)
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return {
content: [
{
type: 'text',
text: `Error: ${message}`,
},
],
isError: true,
}
}
})
// Start server
const transport = new StdioServerTransport()
await server.connect(transport)
// Keep process alive
process.stdin.resume()
}
main().catch((error) => {
console.error('Fatal error:', error)
process.exit(1)
})