Skip to main content
Glama
by Alosies
merge-requests.ts18.9 kB
import type { GitLabClient } from "../client.js"; import type { ListMergeRequestsParams, GetMergeRequestParams, CreateMergeRequestParams, UpdateMergeRequestParams, GetMergeRequestDiffsParams, ListMergeRequestDiffsParams, GetBranchDiffsParams, ListMRNotesParams, ListMRDiscussionsParams, CreateMRNoteParams, CreateMRDiscussionParams, ReplyToMRDiscussionParams, ResolveMRDiscussionParams, UpdateMRDiscussionNoteParams, CreateMRDiscussionNoteParams, DeleteMRDiscussionNoteParams, MarkMRAsDraftParams, MarkMRAsReadyParams, ListMRTemplatesParams, GetMRTemplateParams, GitLabMergeRequest, } from "../types.js"; export class MergeRequestHandlers { constructor(private client: GitLabClient) {} /** * Helper method to resolve merge request IID from source branch name */ private async resolveMergeRequestIid( projectId: string, mergeRequestIid?: number, sourceBranch?: string ): Promise<number> { if (mergeRequestIid) { return mergeRequestIid; } if (!sourceBranch) { throw new Error( "Either merge_request_iid or source_branch must be provided" ); } // Find MR by source branch const mrs = (await this.client.get( `/projects/${encodeURIComponent( projectId )}/merge_requests?source_branch=${encodeURIComponent( sourceBranch )}&state=opened&per_page=1` )) as GitLabMergeRequest[]; if (!mrs || mrs.length === 0) { // Try all states if no open MR found const allMrs = (await this.client.get( `/projects/${encodeURIComponent( projectId )}/merge_requests?source_branch=${encodeURIComponent( sourceBranch )}&per_page=1` )) as GitLabMergeRequest[]; if (!allMrs || allMrs.length === 0) { throw new Error( `No merge request found for source branch: ${sourceBranch}` ); } return allMrs[0].iid; } return mrs[0].iid; } async listMergeRequests(args: ListMergeRequestsParams) { const params = new URLSearchParams(); if (args.state) params.append("state", args.state); if (args.target_branch) params.append("target_branch", args.target_branch); if (args.source_branch) params.append("source_branch", args.source_branch); if (args.assignee_id) params.append("assignee_id", String(args.assignee_id)); if (args.author_id) params.append("author_id", String(args.author_id)); if (args.reviewer_id) params.append("reviewer_id", String(args.reviewer_id)); if (args.reviewer_username) params.append("reviewer_username", args.reviewer_username); if (args.search) params.append("search", args.search); // Only add scope if explicitly provided by user if (args.scope) params.append("scope", args.scope); params.append("per_page", String(args.per_page || 20)); const data = await this.client.get( `/projects/${encodeURIComponent( args.project_id )}/merge_requests?${params.toString()}` ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async getMergeRequest(args: GetMergeRequestParams) { const mergeRequestIid = await this.resolveMergeRequestIid( args.project_id, args.merge_request_iid, args.source_branch ); const data = await this.client.get( `/projects/${encodeURIComponent( args.project_id )}/merge_requests/${mergeRequestIid}` ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async getMergeRequestDiffs(args: GetMergeRequestDiffsParams) { const mergeRequestIid = await this.resolveMergeRequestIid( args.project_id, args.merge_request_iid, args.source_branch ); const params = new URLSearchParams(); if (args.view) params.append("view", args.view); const queryString = params.toString(); const url = `/projects/${encodeURIComponent( args.project_id )}/merge_requests/${mergeRequestIid}/changes${ queryString ? `?${queryString}` : "" }`; const data = await this.client.get(url); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async listMergeRequestDiffs(args: ListMergeRequestDiffsParams) { const mergeRequestIid = await this.resolveMergeRequestIid( args.project_id, args.merge_request_iid, args.source_branch ); const params = new URLSearchParams(); if (args.page) params.append("page", String(args.page)); if (args.per_page) params.append("per_page", String(args.per_page)); if (args.unidiff !== undefined) params.append("unidiff", String(args.unidiff)); const queryString = params.toString(); const url = `/projects/${encodeURIComponent( args.project_id )}/merge_requests/${mergeRequestIid}/diffs${ queryString ? `?${queryString}` : "" }`; const data = await this.client.get(url); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async getBranchDiffs(args: GetBranchDiffsParams) { const params = new URLSearchParams(); params.append("from", args.from); params.append("to", args.to); if (args.straight !== undefined) params.append("straight", String(args.straight)); const url = `/projects/${encodeURIComponent( args.project_id )}/repository/compare?${params.toString()}`; const data = await this.client.get(url); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async createMergeRequest(args: CreateMergeRequestParams) { const requestData: any = { title: args.title, source_branch: args.source_branch, target_branch: args.target_branch, }; if (args.description) { requestData.description = args.description; } if (args.assignee_ids) requestData.assignee_ids = args.assignee_ids; if (args.reviewer_ids) requestData.reviewer_ids = args.reviewer_ids; if (args.labels) requestData.labels = args.labels; if (args.milestone_id) requestData.milestone_id = args.milestone_id; const data = await this.client.post( `/projects/${encodeURIComponent(args.project_id)}/merge_requests`, requestData ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async updateMergeRequest(args: UpdateMergeRequestParams) { const mergeRequestIid = await this.resolveMergeRequestIid( args.project_id, args.merge_request_iid, args.source_branch ); const requestData: any = {}; // Only include provided parameters if (args.title !== undefined) requestData.title = args.title; if (args.description !== undefined) requestData.description = args.description; if (args.state_event !== undefined) requestData.state_event = args.state_event; if (args.target_branch !== undefined) requestData.target_branch = args.target_branch; if (args.assignee_id !== undefined) requestData.assignee_id = args.assignee_id; if (args.assignee_ids !== undefined) requestData.assignee_ids = args.assignee_ids; if (args.reviewer_ids !== undefined) requestData.reviewer_ids = args.reviewer_ids; if (args.milestone_id !== undefined) requestData.milestone_id = args.milestone_id; if (args.labels !== undefined) requestData.labels = args.labels; if (args.remove_source_branch !== undefined) requestData.remove_source_branch = args.remove_source_branch; if (args.squash !== undefined) requestData.squash = args.squash; if (args.allow_collaboration !== undefined) requestData.allow_collaboration = args.allow_collaboration; if (args.merge_when_pipeline_succeeds !== undefined) requestData.merge_when_pipeline_succeeds = args.merge_when_pipeline_succeeds; const data = await this.client.put( `/projects/${encodeURIComponent( args.project_id )}/merge_requests/${mergeRequestIid}`, requestData ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async listMRNotes(args: ListMRNotesParams) { const params = new URLSearchParams(); if (args.sort) params.append("sort", args.sort); if (args.order_by) params.append("order_by", args.order_by); if (args.page) params.append("page", String(args.page)); params.append("per_page", String(args.per_page || 20)); const data = await this.client.get( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }/notes?${params.toString()}` ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async listMRDiscussions(args: ListMRDiscussionsParams) { const encodedProjectId = encodeURIComponent(args.project_id); const perPage = args.per_page || 20; // If unresolved_only is true, fetch all pages and filter if (args.unresolved_only) { const allDiscussions: any[] = []; let page = 1; let hasMore = true; // Fetch all pages (use max per_page for efficiency) while (hasMore) { const { data, headers } = await this.client.getWithHeaders( `/projects/${encodedProjectId}/merge_requests/${args.merge_request_iid}/discussions?per_page=100&page=${page}` ); allDiscussions.push(...data); // Check if there are more pages const nextPage = headers['x-next-page']; hasMore = !!nextPage && nextPage !== ''; page++; // Safety limit to prevent infinite loops if (page > 100) break; } // Filter to only unresolved discussions // A discussion is unresolved if it has resolvable notes and at least one is not resolved const unresolvedDiscussions = allDiscussions.filter((discussion) => { // Check if any note in the discussion is resolvable and not resolved const hasUnresolvedNotes = discussion.notes?.some( (note: any) => note.resolvable === true && note.resolved === false ); return hasUnresolvedNotes; }); return { content: [ { type: 'text', text: JSON.stringify( { discussions: unresolvedDiscussions, metadata: { total_fetched: allDiscussions.length, unresolved_count: unresolvedDiscussions.length, filtered: true, }, }, null, 2 ), }, ], }; } // Normal pagination mode const params = new URLSearchParams(); if (args.page) params.append("page", String(args.page)); params.append("per_page", String(perPage)); const data = await this.client.get( `/projects/${encodedProjectId}/merge_requests/${args.merge_request_iid}/discussions?${params.toString()}` ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async createMRNote(args: CreateMRNoteParams) { const requestData = { body: args.body, }; const data = await this.client.post( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }/notes`, requestData ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async createMRDiscussion(args: CreateMRDiscussionParams) { const requestData: Record<string, unknown> = { body: args.body, }; // Add position for inline/diff comments if (args.position) { requestData.position = { base_sha: args.position.base_sha, start_sha: args.position.start_sha, head_sha: args.position.head_sha, old_path: args.position.old_path, new_path: args.position.new_path, position_type: args.position.position_type || "text", ...(args.position.old_line !== undefined && { old_line: args.position.old_line, }), ...(args.position.new_line !== undefined && { new_line: args.position.new_line, }), }; } const data = await this.client.post( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }/discussions`, requestData ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async replyToMRDiscussion(args: ReplyToMRDiscussionParams) { const requestData = { body: args.body, }; const data = await this.client.post( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }/discussions/${args.discussion_id}/notes`, requestData ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async resolveMRDiscussion(args: ResolveMRDiscussionParams) { const data = await this.client.put( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }/discussions/${args.discussion_id}`, { resolved: true } ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async unresolveMRDiscussion(args: ResolveMRDiscussionParams) { const data = await this.client.put( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }/discussions/${args.discussion_id}`, { resolved: false } ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async updateMRDiscussionNote(args: UpdateMRDiscussionNoteParams) { const data = await this.client.put( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }/discussions/${args.discussion_id}/notes/${args.note_id}`, { body: args.body } ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async createMRDiscussionNote(args: CreateMRDiscussionNoteParams) { const data = await this.client.post( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }/discussions/${args.discussion_id}/notes`, { body: args.body } ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async deleteMRDiscussionNote(args: DeleteMRDiscussionNoteParams) { await this.client.delete( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }/discussions/${args.discussion_id}/notes/${args.note_id}` ); return { content: [ { type: "text", text: JSON.stringify( { message: "Note deleted successfully" }, null, 2 ), }, ], }; } async markMRAsDraft(args: MarkMRAsDraftParams) { // First get the current MR to check its title const mr = (await this.client.get( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }` )) as GitLabMergeRequest; // Check if already a draft if (mr.title.startsWith("Draft: ") || mr.title.startsWith("WIP: ")) { return { content: [ { type: "text", text: JSON.stringify( { message: "Merge request is already marked as draft", ...mr }, null, 2 ), }, ], }; } // Update the title to add Draft: prefix const data = await this.client.put( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }`, { title: `Draft: ${mr.title}` } ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async markMRAsReady(args: MarkMRAsReadyParams) { // First get the current MR to check its title const mr = (await this.client.get( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }` )) as GitLabMergeRequest; // Check if it's a draft and remove the prefix let newTitle = mr.title; if (mr.title.startsWith("Draft: ")) { newTitle = mr.title.replace(/^Draft: /, ""); } else if (mr.title.startsWith("WIP: ")) { newTitle = mr.title.replace(/^WIP: /, ""); } else { return { content: [ { type: "text", text: JSON.stringify( { message: "Merge request is already marked as ready", ...mr }, null, 2 ), }, ], }; } // Update the title to remove the draft prefix const data = await this.client.put( `/projects/${encodeURIComponent(args.project_id)}/merge_requests/${ args.merge_request_iid }`, { title: newTitle } ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async listMRTemplates(args: ListMRTemplatesParams) { const data = await this.client.get( `/projects/${encodeURIComponent( args.project_id )}/templates/merge_requests` ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async getMRTemplate(args: GetMRTemplateParams) { const data = await this.client.get( `/projects/${encodeURIComponent( args.project_id )}/templates/merge_requests/${encodeURIComponent(args.name)}` ); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } }

Latest Blog Posts

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/Alosies/gitlab-mcp-server'

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