From ea06c21f298feb84e93540fa3bfb8b315562fe1f Mon Sep 17 00:00:00 2001 From: Vicen Dominguez Date: Sat, 24 May 2025 12:44:24 +0200 Subject: [PATCH] feat(release): 1.0.44 adds pipeline jobs tool --- README.md | 8 +- index.ts | 276 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- schemas.ts | 118 ++++++++++++++++++++++ 4 files changed, 398 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8b34cc4..d04d6e5 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,6 @@ When using with the Claude App, you need to set up your API key and URLs directl ## Tools 🛠️ + - 1. `create_or_update_file` - Create or update a single file in a GitLab project 2. `search_repositories` - Search for GitLab projects 3. `create_repository` - Create a new GitLab project @@ -93,7 +92,7 @@ When using with the Claude App, you need to set up your API key and URLs directl 15. `mr_discussions` - List discussion items for a merge request 16. `update_merge_request_note` - Modify an existing merge request thread note 17. `create_merge_request_note` - Add a new note to an existing merge request thread -18. `update_issue_note` - Update the content of an existing issue note +18. `update_issue_note` - Modify an existing issue thread note 19. `create_issue_note` - Add a new note to an existing issue thread 20. `list_issues` - List issues in a GitLab project with filtering options 21. `get_issue` - Get details of a specific issue in a GitLab project @@ -121,4 +120,9 @@ When using with the Claude App, you need to set up your API key and URLs directl 43. `update_wiki_page` - Update an existing wiki page in a GitLab project 44. `delete_wiki_page` - Delete a wiki page from a GitLab project 45. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories) +46. `list_pipelines` - List pipelines in a GitLab project with filtering options +47. `get_pipeline` - Get details of a specific pipeline in a GitLab project +48. `list_pipeline_jobs` - List all jobs in a specific pipeline +49. `get_pipeline_job` - Get details of a GitLab pipeline job number +50. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number diff --git a/index.ts b/index.ts index 65d7b98..b1ea41d 100644 --- a/index.ts +++ b/index.ts @@ -17,7 +17,6 @@ import { fileURLToPath } from "url"; import { dirname } from "path"; import fs from "fs"; import path from "path"; - // Add type imports for proxy agents import { Agent } from "http"; import { URL } from "url"; @@ -84,6 +83,15 @@ import { UpdateWikiPageSchema, DeleteWikiPageSchema, GitLabWikiPageSchema, + GetRepositoryTreeSchema, + GitLabTreeItemSchema, + GitLabPipelineSchema, + GetPipelineSchema, + ListPipelinesSchema, + ListPipelineJobsSchema, + // pipeline job schemas + GetPipelineJobOutputSchema, + GitLabPipelineJobSchema, // Discussion Schemas GitLabDiscussionNoteSchema, // Added GitLabDiscussionSchema, @@ -108,6 +116,11 @@ import { type GitLabNamespaceExistsResponse, type GitLabProject, type GitLabLabel, + type GitLabPipeline, + type ListPipelinesOptions, + type GetPipelineOptions, + type ListPipelineJobsOptions, + type GitLabPipelineJob, // Discussion Types type GitLabDiscussionNote, // Added type GitLabDiscussion, @@ -117,8 +130,6 @@ import { type UpdateWikiPageOptions, type DeleteWikiPageOptions, type GitLabWikiPage, - GitLabTreeItemSchema, - GetRepositoryTreeSchema, type GitLabTreeItem, type GetRepositoryTreeOptions, UpdateIssueNoteSchema, @@ -430,6 +441,31 @@ const allTools = [ "Get the repository tree for a GitLab project (list files and directories)", inputSchema: zodToJsonSchema(GetRepositoryTreeSchema), }, + { + name: "list_pipelines", + description: "List pipelines in a GitLab project with filtering options", + inputSchema: zodToJsonSchema(ListPipelinesSchema), + }, + { + name: "get_pipeline", + description: "Get details of a specific pipeline in a GitLab project", + inputSchema: zodToJsonSchema(GetPipelineSchema), + }, + { + name: "list_pipeline_jobs", + description: "List all jobs in a specific pipeline", + inputSchema: zodToJsonSchema(ListPipelineJobsSchema), + }, + { + name: "get_pipeline_job", + description: "Get details of a GitLab pipeline job number", + inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), + }, + { + name: "get_pipeline_job_output", + description: "Get the output/trace of a GitLab pipeline job number", + inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), + }, ]; // Define which tools are read-only @@ -448,6 +484,11 @@ const readOnlyTools = [ "get_namespace", "verify_namespace", "get_project", + "get_pipeline", + "list_pipelines", + "list_pipeline_jobs", + "get_pipeline_job", + "get_pipeline_job_output", "list_projects", "list_labels", "get_label", @@ -2300,6 +2341,166 @@ async function deleteWikiPage(projectId: string, slug: string): Promise { await handleGitLabError(response); } +/** + * List pipelines in a GitLab project + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {ListPipelinesOptions} options - Options for filtering pipelines + * @returns {Promise} List of pipelines + */ +async function listPipelines( + projectId: string, + options: Omit = {} +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines` + ); + + // Add all query parameters + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.append(key, value.toString()); + } + }); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabPipelineSchema).parse(data); +} + +/** + * Get details of a specific pipeline + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} pipelineId - The ID of the pipeline + * @returns {Promise} Pipeline details + */ +async function getPipeline( + projectId: string, + pipelineId: number +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + if (response.status === 404) { + throw new Error(`Pipeline not found`); + } + + await handleGitLabError(response); + const data = await response.json(); + return GitLabPipelineSchema.parse(data); +} + +/** + * List all jobs in a specific pipeline + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} pipelineId - The ID of the pipeline + * @param {Object} options - Options for filtering jobs + * @returns {Promise} List of pipeline jobs + */ +async function listPipelineJobs( + projectId: string, + pipelineId: number, + options: Omit = {} +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs` + ); + + // Add all query parameters + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + if (typeof value === "boolean") { + url.searchParams.append(key, value ? "true" : "false"); + } else { + url.searchParams.append(key, value.toString()); + } + } + }); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + if (response.status === 404) { + throw new Error(`Pipeline not found`); + } + + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabPipelineJobSchema).parse(data); +} +async function getPipelineJob( + projectId: string, + jobId: number +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/jobs/${jobId}` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + if (response.status === 404) { + throw new Error(`Job not found`); + } + + await handleGitLabError(response); + const data = await response.json(); + return GitLabPipelineJobSchema.parse(data); +} + +/** + * Get the output/trace of a pipeline job + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} jobId - The ID of the job + * @returns {Promise} The job output/trace + */ +async function getPipelineJobOutput( + projectId: string, + jobId: number +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/jobs/${jobId}/trace` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + headers: { + ...DEFAULT_HEADERS, + Accept: "text/plain", // Override Accept header to get plain text + }, + }); + + if (response.status === 404) { + throw new Error(`Job trace not found or job is not finished yet`); + } + + await handleGitLabError(response); + return await response.text(); +} + /** * Get the repository tree for a project * @param {string} projectId - The ID or URL-encoded path of the project @@ -3030,6 +3231,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "list_pipelines": { + const args = ListPipelinesSchema.parse(request.params.arguments); + const { project_id, ...options } = args; + const pipelines = await listPipelines(project_id, options); + return { + content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }], + }; + } + + case "get_pipeline": { + const { project_id, pipeline_id } = GetPipelineSchema.parse( + request.params.arguments + ); + const pipeline = await getPipeline(project_id, pipeline_id); + return { + content: [ + { + type: "text", + text: JSON.stringify(pipeline, null, 2), + }, + ], + }; + } + + case "list_pipeline_jobs": { + const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse( + request.params.arguments + ); + const jobs = await listPipelineJobs(project_id, pipeline_id, options); + return { + content: [ + { + type: "text", + text: JSON.stringify(jobs, null, 2), + }, + ], + }; + } + + case "get_pipeline_job": { + const { project_id, job_id } = GetPipelineJobOutputSchema.parse( + request.params.arguments + ); + const jobDetails = await getPipelineJob(project_id, job_id); + return { + content: [ + { + type: "text", + text: JSON.stringify(jobDetails, null, 2), + }, + ], + }; + } + + case "get_pipeline_job_output": { + const { project_id, job_id } = GetPipelineJobOutputSchema.parse( + request.params.arguments + ); + const jobOutput = await getPipelineJobOutput(project_id, job_id); + return { + content: [ + { + type: "text", + text: jobOutput, + }, + ], + }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/package.json b/package.json index 064aa3f..c76f7a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.43", + "version": "1.0.44", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", diff --git a/schemas.ts b/schemas.ts index 6ae2fce..f8154ef 100644 --- a/schemas.ts +++ b/schemas.ts @@ -7,6 +7,119 @@ export const GitLabAuthorSchema = z.object({ date: z.string(), }); +// Pipeline related schemas +export const GitLabPipelineSchema = z.object({ + id: z.number(), + project_id: z.number(), + sha: z.string(), + ref: z.string(), + status: z.string(), + source: z.string().optional(), + created_at: z.string(), + updated_at: z.string(), + web_url: z.string(), + duration: z.number().nullable().optional(), + started_at: z.string().nullable().optional(), + finished_at: z.string().nullable().optional(), + coverage: z.number().nullable().optional(), + user: z.object({ + id: z.number(), + name: z.string(), + username: z.string(), + avatar_url: z.string().nullable().optional(), + }).optional(), + detailed_status: z.object({ + icon: z.string().optional(), + text: z.string().optional(), + label: z.string().optional(), + group: z.string().optional(), + tooltip: z.string().optional(), + has_details: z.boolean().optional(), + details_path: z.string().optional(), + illustration: z.object({ + image: z.string().optional(), + size: z.string().optional(), + title: z.string().optional(), + }).optional(), + favicon: z.string().optional(), + }).optional(), +}); + +// Pipeline job related schemas +export const GitLabPipelineJobSchema = z.object({ + id: z.number(), + status: z.string(), + stage: z.string(), + name: z.string(), + ref: z.string(), + tag: z.boolean(), + coverage: z.number().nullable().optional(), + created_at: z.string(), + started_at: z.string().nullable().optional(), + finished_at: z.string().nullable().optional(), + duration: z.number().nullable().optional(), + user: z.object({ + id: z.number(), + name: z.string(), + username: z.string(), + avatar_url: z.string().nullable().optional(), + }).optional(), + commit: z.object({ + id: z.string(), + short_id: z.string(), + title: z.string(), + author_name: z.string(), + author_email: z.string(), + }).optional(), + pipeline: z.object({ + id: z.number(), + project_id: z.number(), + status: z.string(), + ref: z.string(), + sha: z.string(), + }).optional(), + web_url: z.string().optional(), +}); + +// Schema for listing pipelines +export const ListPipelinesSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + scope: z.enum(['running', 'pending', 'finished', 'branches', 'tags']).optional().describe("The scope of pipelines"), + status: z.enum(['created', 'waiting_for_resource', 'preparing', 'pending', 'running', 'success', 'failed', 'canceled', 'skipped', 'manual', 'scheduled']).optional().describe("The status of pipelines"), + ref: z.string().optional().describe("The ref of pipelines"), + sha: z.string().optional().describe("The SHA of pipelines"), + yaml_errors: z.boolean().optional().describe("Returns pipelines with invalid configurations"), + username: z.string().optional().describe("The username of the user who triggered pipelines"), + updated_after: z.string().optional().describe("Return pipelines updated after the specified date"), + updated_before: z.string().optional().describe("Return pipelines updated before the specified date"), + order_by: z.enum(['id', 'status', 'ref', 'updated_at', 'user_id']).optional().describe("Order pipelines by"), + sort: z.enum(['asc', 'desc']).optional().describe("Sort pipelines"), + page: z.number().optional().describe("Page number for pagination"), + per_page: z.number().optional().describe("Number of items per page (max 100)"), +}); + +// Schema for getting a specific pipeline +export const GetPipelineSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + pipeline_id: z.number().describe("The ID of the pipeline"), +}); + +// Schema for listing jobs in a pipeline +export const ListPipelineJobsSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + pipeline_id: z.number().describe("The ID of the pipeline"), + scope: z.enum(['created', 'pending', 'running', 'failed', 'success', 'canceled', 'skipped', 'manual']).optional().describe("The scope of jobs to show"), + include_retried: z.boolean().optional().describe("Whether to include retried jobs"), + page: z.number().optional().describe("Page number for pagination"), + per_page: z.number().optional().describe("Number of items per page (max 100)"), +}); + +// Schema for the input parameters for pipeline job operations +export const GetPipelineJobOutputSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + job_id: z.number().describe("The ID of the job"), +}); + // Namespace related schemas // Base schema for project-related operations @@ -1120,3 +1233,8 @@ export type GetRepositoryTreeOptions = z.infer; export type MergeRequestThreadPosition = z.infer; export type CreateMergeRequestThreadOptions = z.infer; export type CreateMergeRequestNoteOptions = z.infer; +export type GitLabPipelineJob = z.infer; +export type GitLabPipeline = z.infer; +export type ListPipelinesOptions = z.infer; +export type GetPipelineOptions = z.infer; +export type ListPipelineJobsOptions = z.infer;