UNPKG

@yoda.digital/gitlab-mcp-server

Version:

GitLab MCP Server - A Model Context Protocol server for GitLab integration

992 lines 42.3 kB
import fetch from "node-fetch"; import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabContentSchema, GitLabSearchResponseSchema, GitLabGroupProjectsResponseSchema, GitLabCommitSchema, GitLabEventsResponseSchema, GitLabCommitsResponseSchema, GitLabIssuesResponseSchema, GitLabMergeRequestsResponseSchema, GitLabWikiPageSchema, GitLabWikiPagesResponseSchema, GitLabWikiAttachmentSchema, GitLabMembersResponseSchema, GitLabNotesResponseSchema, GitLabDiscussionsResponseSchema, } from './schemas.js'; /** * GitLab API client for interacting with GitLab resources */ export class GitLabApi { apiUrl; token; constructor(config) { this.apiUrl = config.apiUrl; this.token = config.token; } /** * Forks a GitLab project to a specified namespace. * * @param projectId - The ID or URL-encoded path of the project to fork * @param namespace - Optional namespace to fork the project into * @returns A promise that resolves to the forked project details * @throws Will throw an error if the GitLab API request fails */ async forkProject(projectId, namespace) { const url = `${this.apiUrl}/projects/${encodeURIComponent(projectId)}/fork`; const queryParams = namespace ? `?namespace=${encodeURIComponent(namespace)}` : ''; const response = await fetch(url + queryParams, { method: "POST", headers: { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json" } }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } return GitLabForkSchema.parse(await response.json()); } /** * Creates a new branch in a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param options - Options for creating the branch, including name and ref * @returns A promise that resolves to the created branch details * @throws Will throw an error if the GitLab API request fails */ async createBranch(projectId, options) { const response = await fetch(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/repository/branches`, { method: "POST", headers: { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json" }, body: JSON.stringify({ branch: options.name, ref: options.ref }) }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } return GitLabReferenceSchema.parse(await response.json()); } /** * Retrieves the default branch reference for a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @returns A promise that resolves to the default branch reference * @throws Will throw an error if the GitLab API request fails */ async getDefaultBranchRef(projectId) { const response = await fetch(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}`, { headers: { "Authorization": `Bearer ${this.token}` } }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } const project = GitLabRepositorySchema.parse(await response.json()); return project.default_branch; } /** * Retrieves the contents of a file from a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param filePath - The path of the file within the project * @param ref - The name of the branch, tag, or commit * @returns A promise that resolves to the file contents * @throws Will throw an error if the GitLab API request fails */ async getFileContents(projectId, filePath, ref) { const encodedPath = encodeURIComponent(filePath); const url = `${this.apiUrl}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}?ref=${encodeURIComponent(ref)}`; const response = await fetch(url, { headers: { "Authorization": `Bearer ${this.token}` } }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } const data = GitLabContentSchema.parse(await response.json()); if (!Array.isArray(data) && data.content) { data.content = Buffer.from(data.content, 'base64').toString('utf8'); } return data; } /** * Creates or updates a file in a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param filePath - The path of the file within the project * @param content - The content of the file * @param commitMessage - The commit message for the change * @param branch - The branch to commit the change to * @param previousPath - Optional previous path if the file is being renamed * @returns A promise that resolves to the created or updated file details * @throws Will throw an error if the GitLab API request fails */ async createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath) { const encodedPath = encodeURIComponent(filePath); const url = `${this.apiUrl}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`; const body = { branch, content, commit_message: commitMessage, ...(previousPath ? { previous_path: previousPath } : {}) }; // Check if file exists let method = "POST"; try { await this.getFileContents(projectId, filePath, branch); method = "PUT"; } catch (error) { // File doesn't exist, use POST } const response = await fetch(url, { method, headers: { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json" }, body: JSON.stringify(body) }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } const responseData = await response.json(); return { file_path: filePath, branch: branch, commit_id: responseData.commit_id || responseData.id || "unknown", content: responseData.content }; } /** * Creates a commit in a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param message - The commit message * @param branch - The branch to commit the changes to * @param actions - An array of file operations to include in the commit * @returns A promise that resolves to the created commit details * @throws Will throw an error if the GitLab API request fails */ async createCommit(projectId, message, branch, actions) { const response = await fetch(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/repository/commits`, { method: "POST", headers: { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json" }, body: JSON.stringify({ branch, commit_message: message, actions: actions.map(action => ({ action: "create", file_path: action.path, content: action.content })) }) }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } return GitLabCommitSchema.parse(await response.json()); } /** * Searches for GitLab projects based on a query. * * @param query - The search query * @param page - The page number to retrieve (default is 1) * @param perPage - The number of results per page (default is 20) * @returns A promise that resolves to the search results * @throws Will throw an error if the GitLab API request fails */ async searchProjects(query, page = 1, perPage = 20) { const url = new URL(`${this.apiUrl}/projects`); url.searchParams.append("search", query); url.searchParams.append("page", page.toString()); url.searchParams.append("per_page", perPage.toString()); const response = await fetch(url.toString(), { headers: { "Authorization": `Bearer ${this.token}` } }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } const projects = await response.json(); return GitLabSearchResponseSchema.parse({ count: parseInt(response.headers.get("X-Total") || "0"), items: projects }); } /** * Lists all projects (repositories) within a specific GitLab group. * * @param groupId - The ID or URL-encoded path of the group * @param options - Optional parameters for filtering and pagination * @returns A promise that resolves to the list of group projects * @throws Will throw an error if the GitLab API request fails */ async listGroupProjects(groupId, options = {}) { const url = new URL(`${this.apiUrl}/groups/${encodeURIComponent(groupId)}/projects`); // Add query parameters Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()); } }); const response = await fetch(url.toString(), { headers: { "Authorization": `Bearer ${this.token}` } }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } const projects = await response.json(); const totalCount = parseInt(response.headers.get("X-Total") || "0"); return GitLabGroupProjectsResponseSchema.parse({ count: totalCount, items: projects }); } /** * Retrieves events for a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param options - Optional parameters for filtering and pagination * @returns A promise that resolves to the events response * @throws Will throw an error if the GitLab API request fails */ async getProjectEvents(projectId, options = {}) { const url = new URL(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/events`); // Add query parameters for filtering and pagination Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()); } }); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const events = await response.json(); // Get the total count from the headers const totalCount = parseInt(response.headers.get("X-Total") || "0"); // Validate and return the response return GitLabEventsResponseSchema.parse({ count: totalCount, items: events, }); } /** * Retrieves commits for a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param options - Optional parameters for filtering and pagination * @returns A promise that resolves to the commits response * @throws Will throw an error if the GitLab API request fails */ async listCommits(projectId, options = {}) { const url = new URL(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/repository/commits`); // Add query parameters for filtering and pagination Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()); } }); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const commits = await response.json(); // Get the total count from the headers const totalCount = parseInt(response.headers.get("X-Total") || "0"); // Validate and return the response return GitLabCommitsResponseSchema.parse({ count: totalCount, items: commits, }); } /** * Retrieves issues for a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param options - Optional parameters for filtering and pagination * @returns A promise that resolves to the issues response * @throws Will throw an error if the GitLab API request fails */ async listIssues(projectId, options = {}) { // Extract iid for client-side filtering if provided const { iid, ...apiOptions } = options; // Construct the URL with the project ID const url = new URL(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/issues`); // Add all query parameters except iid (we'll filter that client-side) Object.entries(apiOptions).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()); } }); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const issues = await response.json(); // If iid is provided, filter the issues by iid const filteredIssues = iid !== undefined ? issues.filter(issue => issue.iid?.toString() === iid.toString()) : issues; // Get the total count - if filtered, use the filtered length const totalCount = iid !== undefined ? filteredIssues.length : parseInt(response.headers.get("X-Total") || "0"); // Validate and return the response return GitLabIssuesResponseSchema.parse({ count: totalCount, items: filteredIssues, }); } /** * Retrieves merge requests for a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param options - Optional parameters for filtering and pagination * @returns A promise that resolves to the merge requests response * @throws Will throw an error if the GitLab API request fails */ async listMergeRequests(projectId, options = {}) { const url = new URL(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/merge_requests`); // 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(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const mergeRequests = await response.json(); // Get the total count from the headers const totalCount = parseInt(response.headers.get("X-Total") || "0"); // Validate and return the response return GitLabMergeRequestsResponseSchema.parse({ count: totalCount, items: mergeRequests, }); } /** * Creates a new issue in a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param options - Options for creating the issue, including title, description, assignee IDs, milestone ID, and labels * @returns A promise that resolves to the created issue details * @throws Will throw an error if the GitLab API request fails */ async createIssue(projectId, options) { const response = await fetch(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/issues`, { method: "POST", headers: { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json" }, body: JSON.stringify({ title: options.title, description: options.description, assignee_ids: options.assignee_ids, milestone_id: options.milestone_id, labels: options.labels?.join(',') }) }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } return GitLabIssueSchema.parse(await response.json()); } /** * Creates a new merge request in a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param options - Options for creating the merge request, including title, description, source branch, target branch, allow collaboration, and draft status * @returns A promise that resolves to the created merge request details * @throws Will throw an error if the GitLab API request fails */ async createMergeRequest(projectId, options) { const response = await fetch(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/merge_requests`, { method: "POST", headers: { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json" }, body: JSON.stringify({ title: options.title, description: options.description, source_branch: options.source_branch, target_branch: options.target_branch, allow_collaboration: options.allow_collaboration, draft: options.draft }) }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } const responseData = await response.json(); return { id: responseData.id, iid: responseData.iid, project_id: responseData.project_id, title: responseData.title, description: responseData.description || null, state: responseData.state, merged: responseData.merged, author: responseData.author, assignees: responseData.assignees || [], source_branch: responseData.source_branch, target_branch: responseData.target_branch, diff_refs: responseData.diff_refs || null, web_url: responseData.web_url, created_at: responseData.created_at, updated_at: responseData.updated_at, merged_at: responseData.merged_at, closed_at: responseData.closed_at, merge_commit_sha: responseData.merge_commit_sha }; } /** * Creates a new repository in GitLab. * * @param options - Options for creating the repository, including name, description, visibility, and initialization with README * @returns A promise that resolves to the created repository details * @throws Will throw an error if the GitLab API request fails */ async createRepository(options) { const response = await fetch(`${this.apiUrl}/projects`, { method: "POST", headers: { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json" }, body: JSON.stringify({ name: options.name, description: options.description, visibility: options.visibility, initialize_with_readme: options.initialize_with_readme }) }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } return GitLabRepositorySchema.parse(await response.json()); } /** * Lists all wiki pages for a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param options - Optional parameters for the request * @returns A promise that resolves to the wiki pages response * @throws Will throw an error if the GitLab API request fails */ async listProjectWikiPages(projectId, options = {}) { const url = new URL(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/wikis`); // Add query parameters if (options.with_content) { url.searchParams.append("with_content", "true"); } const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const wikiPages = await response.json(); // Validate and return the response return GitLabWikiPagesResponseSchema.parse({ count: wikiPages.length, items: wikiPages, }); } /** * Gets a specific wiki page for a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param slug - The slug of the wiki page * @param options - Optional parameters for the request * @returns A promise that resolves to the wiki page * @throws Will throw an error if the GitLab API request fails */ async getProjectWikiPage(projectId, slug, options = {}) { const url = new URL(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(slug)}`); // Add query parameters if (options.render_html) { url.searchParams.append("render_html", "true"); } if (options.version) { url.searchParams.append("version", options.version); } const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const wikiPage = await response.json(); // Validate and return the response return GitLabWikiPageSchema.parse(wikiPage); } /** * Creates a new wiki page for a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param options - Options for creating the wiki page * @returns A promise that resolves to the created wiki page * @throws Will throw an error if the GitLab API request fails */ async createProjectWikiPage(projectId, options) { const response = await fetch(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/wikis`, { method: "POST", headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ title: options.title, content: options.content, format: options.format || "markdown", }), }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const wikiPage = await response.json(); // Validate and return the response return GitLabWikiPageSchema.parse(wikiPage); } /** * Edits an existing wiki page for a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param slug - The slug of the wiki page * @param options - Options for editing the wiki page * @returns A promise that resolves to the edited wiki page * @throws Will throw an error if the GitLab API request fails */ async editProjectWikiPage(projectId, slug, options) { const response = await fetch(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(slug)}`, { method: "PUT", headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ title: options.title, content: options.content, format: options.format, }), }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const wikiPage = await response.json(); // Validate and return the response return GitLabWikiPageSchema.parse(wikiPage); } /** * Deletes a wiki page from a GitLab project. * * @param projectId - The ID or URL-encoded path of the project * @param slug - The slug of the wiki page * @returns A promise that resolves when the wiki page is deleted * @throws Will throw an error if the GitLab API request fails */ async deleteProjectWikiPage(projectId, slug) { const response = await fetch(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(slug)}`, { method: "DELETE", headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } } /** * Uploads an attachment to a GitLab project wiki. * * @param projectId - The ID or URL-encoded path of the project * @param options - Options for uploading the attachment * @returns A promise that resolves to the uploaded attachment details * @throws Will throw an error if the GitLab API request fails */ async uploadProjectWikiAttachment(projectId, options) { // Convert content to base64 if it's not already const content = options.content.startsWith("data:") ? options.content : `data:application/octet-stream;base64,${Buffer.from(options.content).toString('base64')}`; const response = await fetch(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/wikis/attachments`, { method: "POST", headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ file_name: options.file_path.split('/').pop(), file_path: options.file_path, content: content, branch: options.branch, }), }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const attachment = await response.json(); // Validate and return the response return GitLabWikiAttachmentSchema.parse(attachment); } /** * Lists all wiki pages for a GitLab group. * * @param groupId - The ID or URL-encoded path of the group * @param options - Optional parameters for the request * @returns A promise that resolves to the wiki pages response * @throws Will throw an error if the GitLab API request fails */ async listGroupWikiPages(groupId, options = {}) { const url = new URL(`${this.apiUrl}/groups/${encodeURIComponent(groupId)}/wikis`); // Add query parameters if (options.with_content) { url.searchParams.append("with_content", "true"); } const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const wikiPages = await response.json(); // Validate and return the response return GitLabWikiPagesResponseSchema.parse({ count: wikiPages.length, items: wikiPages, }); } /** * Gets a specific wiki page for a GitLab group. * * @param groupId - The ID or URL-encoded path of the group * @param slug - The slug of the wiki page * @param options - Optional parameters for the request * @returns A promise that resolves to the wiki page * @throws Will throw an error if the GitLab API request fails */ async getGroupWikiPage(groupId, slug, options = {}) { const url = new URL(`${this.apiUrl}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`); // Add query parameters if (options.render_html) { url.searchParams.append("render_html", "true"); } if (options.version) { url.searchParams.append("version", options.version); } const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const wikiPage = await response.json(); // Validate and return the response return GitLabWikiPageSchema.parse(wikiPage); } /** * Creates a new wiki page for a GitLab group. * * @param groupId - The ID or URL-encoded path of the group * @param options - Options for creating the wiki page * @returns A promise that resolves to the created wiki page * @throws Will throw an error if the GitLab API request fails */ async createGroupWikiPage(groupId, options) { const response = await fetch(`${this.apiUrl}/groups/${encodeURIComponent(groupId)}/wikis`, { method: "POST", headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ title: options.title, content: options.content, format: options.format || "markdown", }), }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const wikiPage = await response.json(); // Validate and return the response return GitLabWikiPageSchema.parse(wikiPage); } /** * Edits an existing wiki page for a GitLab group. * * @param groupId - The ID or URL-encoded path of the group * @param slug - The slug of the wiki page * @param options - Options for editing the wiki page * @returns A promise that resolves to the edited wiki page * @throws Will throw an error if the GitLab API request fails */ async editGroupWikiPage(groupId, slug, options) { const response = await fetch(`${this.apiUrl}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`, { method: "PUT", headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ title: options.title, content: options.content, format: options.format, }), }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const wikiPage = await response.json(); // Validate and return the response return GitLabWikiPageSchema.parse(wikiPage); } /** * Deletes a wiki page from a GitLab group. * * @param groupId - The ID or URL-encoded path of the group * @param slug - The slug of the wiki page * @returns A promise that resolves when the wiki page is deleted * @throws Will throw an error if the GitLab API request fails */ async deleteGroupWikiPage(groupId, slug) { const response = await fetch(`${this.apiUrl}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`, { method: "DELETE", headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } } /** * Uploads an attachment to a GitLab group wiki. * * @param groupId - The ID or URL-encoded path of the group * @param options - Options for uploading the attachment * @returns A promise that resolves to the uploaded attachment details * @throws Will throw an error if the GitLab API request fails */ async uploadGroupWikiAttachment(groupId, options) { // Convert content to base64 if it's not already const content = options.content.startsWith("data:") ? options.content : `data:application/octet-stream;base64,${Buffer.from(options.content).toString('base64')}`; const response = await fetch(`${this.apiUrl}/groups/${encodeURIComponent(groupId)}/wikis/attachments`, { method: "POST", headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ file_name: options.file_path.split('/').pop(), file_path: options.file_path, content: content, branch: options.branch, }), }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } // Parse the response JSON const attachment = await response.json(); // Validate and return the response return GitLabWikiAttachmentSchema.parse(attachment); } /** * Lists members of a GitLab project (including inherited members). * * @param projectId - The ID or URL-encoded path of the project * @param options - Options for listing members * @returns A promise that resolves to the members response * @throws Will throw an error if the GitLab API request fails */ async listProjectMembers(projectId, options = {}) { const url = new URL(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/members/all`); // Add query parameters Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()); } }); const response = await fetch(url.toString(), { headers: { "Authorization": `Bearer ${this.token}` } }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } const data = await response.json(); const totalCount = parseInt(response.headers.get("X-Total") || "0"); return GitLabMembersResponseSchema.parse({ count: totalCount, items: data }); } /** * Lists members of a GitLab group (including inherited members). * * @param groupId - The ID or URL-encoded path of the group * @param options - Options for listing members * @returns A promise that resolves to the members response * @throws Will throw an error if the GitLab API request fails */ async listGroupMembers(groupId, options = {}) { const url = new URL(`${this.apiUrl}/groups/${encodeURIComponent(groupId)}/members/all`); // Add query parameters Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()); } }); const response = await fetch(url.toString(), { headers: { "Authorization": `Bearer ${this.token}` } }); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `GitLab API error: ${response.statusText}`); } const data = await response.json(); const totalCount = parseInt(response.headers.get("X-Total") || "0"); return GitLabMembersResponseSchema.parse({ count: totalCount, items: data }); } /** * Retrieves notes for a GitLab issue. * * @param projectId - The ID or URL-encoded path of the project * @param issueIid - The internal ID of the issue * @param options - Optional parameters for filtering and pagination * @returns A promise that resolves to the notes response * @throws Will throw an error if the GitLab API request fails */ async getIssueNotes(projectId, issueIid, options = {}) { const url = new URL(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/notes`); // Add query parameters for filtering and pagination Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()); } }); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { let errorMessage = `GitLab API error: ${response.statusText}`; if (response.status === 404) { errorMessage = `Issue not found: Project ID ${projectId}, Issue IID ${issueIid}`; } else if (response.status === 403) { errorMessage = `Permission denied to access issue notes`; } else if (response.status === 429) { errorMessage = `GitLab API rate limit exceeded`; } throw new McpError(ErrorCode.InternalError, errorMessage); } // Parse the response JSON const notes = await response.json(); // Get the total count from the headers const totalCount = parseInt(response.headers.get("X-Total") || "0"); // Validate and return the response return GitLabNotesResponseSchema.parse({ count: totalCount, items: notes, }); } /** * Retrieves discussions for a GitLab issue. * * @param projectId - The ID or URL-encoded path of the project * @param issueIid - The internal ID of the issue * @param options - Optional parameters for pagination * @returns A promise that resolves to the discussions response * @throws Will throw an error if the GitLab API request fails */ async getIssueDiscussions(projectId, issueIid, options = {}) { const url = new URL(`${this.apiUrl}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/discussions`); // Add query parameters for pagination Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()); } }); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!response.ok) { let errorMessage = `GitLab API error: ${response.statusText}`; if (response.status === 404) { errorMessage = `Issue not found: Project ID ${projectId}, Issue IID ${issueIid}`; } else if (response.status === 403) { errorMessage = `Permission denied to access issue discussions`; } else if (response.status === 429) { errorMessage = `GitLab API rate limit exceeded`; } throw new McpError(ErrorCode.InternalError, errorMessage); } // Parse the response JSON const discussions = await response.json(); // Get the total count from the headers const totalCount = parseInt(response.headers.get("X-Total") || "0"); // Validate and return the response return GitLabDiscussionsResponseSchema.parse({ count: totalCount, items: discussions, }); } } //# sourceMappingURL=gitlab-api.js.map