diff --git a/README.md b/README.md index d04d6e5..42d679e 100644 --- a/README.md +++ b/README.md @@ -125,4 +125,5 @@ When using with the Claude App, you need to set up your API key and URLs directl 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 +51. `list_merge_requests` - List merge requests in a GitLab project with filtering options diff --git a/index.ts b/index.ts index b1ea41d..a6b00a6 100644 --- a/index.ts +++ b/index.ts @@ -134,6 +134,7 @@ import { type GetRepositoryTreeOptions, UpdateIssueNoteSchema, CreateIssueNoteSchema, + ListMergeRequestsSchema, } from "./schemas.js"; /** @@ -466,6 +467,11 @@ const allTools = [ description: "Get the output/trace of a GitLab pipeline job number", inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), }, + { + name: "list_merge_requests", + description: "List merge requests in a GitLab project with filtering options", + inputSchema: zodToJsonSchema(ListMergeRequestsSchema), + }, ]; // Define which tools are read-only @@ -476,6 +482,7 @@ const readOnlyTools = [ "get_merge_request_diffs", "mr_discussions", "list_issues", + "list_merge_requests", "get_issue", "list_issue_links", "list_issue_discussions", @@ -791,6 +798,43 @@ async function listIssues( return z.array(GitLabIssueSchema).parse(data); } +/** + * List merge requests in a GitLab project with optional filtering + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {Object} options - Optional filtering parameters + * @returns {Promise} List of merge requests + */ +async function listMergeRequests( + projectId: string, + options: Omit, "project_id"> = {} +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests` + ); + + // Add all query parameters + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + if (key === "labels" && Array.isArray(value)) { + // Handle array of labels + url.searchParams.append(key, value.join(",")); + } else { + 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(GitLabMergeRequestSchema).parse(data); +} + /** * Get a single issue from a GitLab project * 단일 이슈 조회 @@ -3300,6 +3344,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "list_merge_requests": { + const args = ListMergeRequestsSchema.parse(request.params.arguments); + const mergeRequests = await listMergeRequests(args.project_id, args); + return { + content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }], + }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/package-lock.json b/package-lock.json index 991408d..2002944 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.38", + "version": "1.0.46", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zereight/mcp-gitlab", - "version": "1.0.38", + "version": "1.0.46", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.8.0", diff --git a/schemas.ts b/schemas.ts index 9393db0..c3d868c 100644 --- a/schemas.ts +++ b/schemas.ts @@ -834,6 +834,88 @@ export const ListIssuesSchema = z.object({ per_page: z.number().optional().describe("Number of items per page"), }); +// Merge Requests API operation schemas +export const ListMergeRequestsSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + assignee_id: z + .number() + .optional() + .describe("Returns merge requests assigned to the given user ID"), + assignee_username: z + .string() + .optional() + .describe("Returns merge requests assigned to the given username"), + author_id: z + .number() + .optional() + .describe("Returns merge requests created by the given user ID"), + author_username: z + .string() + .optional() + .describe("Returns merge requests created by the given username"), + reviewer_id: z + .number() + .optional() + .describe("Returns merge requests which have the user as a reviewer"), + reviewer_username: z + .string() + .optional() + .describe("Returns merge requests which have the user as a reviewer"), + created_after: z + .string() + .optional() + .describe("Return merge requests created after the given time"), + created_before: z + .string() + .optional() + .describe("Return merge requests created before the given time"), + updated_after: z + .string() + .optional() + .describe("Return merge requests updated after the given time"), + updated_before: z + .string() + .optional() + .describe("Return merge requests updated before the given time"), + labels: z.array(z.string()).optional().describe("Array of label names"), + milestone: z.string().optional().describe("Milestone title"), + scope: z + .enum(["created_by_me", "assigned_to_me", "all"]) + .optional() + .describe("Return merge requests from a specific scope"), + search: z.string().optional().describe("Search for specific terms"), + state: z + .enum(["opened", "closed", "locked", "merged", "all"]) + .optional() + .describe("Return merge requests with a specific state"), + order_by: z + .enum(["created_at", "updated_at", "priority", "label_priority", "milestone_due", "popularity"]) + .optional() + .describe("Return merge requests ordered by the given field"), + sort: z + .enum(["asc", "desc"]) + .optional() + .describe("Return merge requests sorted in ascending or descending order"), + target_branch: z + .string() + .optional() + .describe("Return merge requests targeting a specific branch"), + source_branch: z + .string() + .optional() + .describe("Return merge requests from a specific source branch"), + wip: z + .enum(["yes", "no"]) + .optional() + .describe("Filter merge requests against their wip status"), + with_labels_details: z + .boolean() + .optional() + .describe("Return more details for each label"), + page: z.number().optional().describe("Page number for pagination"), + per_page: z.number().optional().describe("Number of items per page"), +}); + export const GetIssueSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of the project issue"),