diff --git a/README.md b/README.md index 43f3f32..cf43aac 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,6 @@ $ sh scripts/image_push.sh docker_user_name ## 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 @@ -103,56 +102,58 @@ $ sh scripts/image_push.sh docker_user_name 9. `create_branch` - Create a new branch in a GitLab project 10. `get_merge_request` - Get details of a merge request (Either mergeRequestIid or branchName must be provided) 11. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided) -12. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided) -13. `create_note` - Create a new note (comment) to an issue or merge request -14. `create_merge_request_thread` - Create a new thread on a merge request -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` - 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 -22. `update_issue` - Update an issue in a GitLab project -23. `delete_issue` - Delete an issue from a GitLab project -24. `list_issue_links` - List all issue links for a specific issue -25. `list_issue_discussions` - List discussions for an issue in a GitLab project -26. `get_issue_link` - Get a specific issue link -27. `create_issue_link` - Create an issue link between two issues -28. `delete_issue_link` - Delete an issue link -29. `list_namespaces` - List all namespaces available to the current user -30. `get_namespace` - Get details of a namespace by ID or path -31. `verify_namespace` - Verify if a namespace path exists -32. `get_project` - Get details of a specific project -33. `list_projects` - List projects accessible by the current user -34. `list_labels` - List labels for a project -35. `get_label` - Get a single label from a project -36. `create_label` - Create a new label in a project -37. `update_label` - Update an existing label in a project -38. `delete_label` - Delete a label from a project -39. `list_group_projects` - List projects in a GitLab group with filtering options -40. `list_wiki_pages` - List wiki pages in a GitLab project -41. `get_wiki_page` - Get details of a specific wiki page -42. `create_wiki_page` - Create a new wiki page in a GitLab project -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 -51. `create_pipeline` - Create a new pipeline for a branch or tag -52. `retry_pipeline` - Retry a failed or canceled pipeline -53. `cancel_pipeline` - Cancel a running pipeline -54. `list_merge_requests` - List merge requests in a GitLab project with filtering options -55. `list_milestones` - List milestones in a GitLab project with filtering options -56. `get_milestone` - Get details of a specific milestone -57. `create_milestone` - Create a new milestone in a GitLab project -58. `edit_milestone ` - Edit an existing milestone in a GitLab project -59. `delete_milestone` - Delete a milestone from a GitLab project -60. `get_milestone_issue` - Get issues associated with a specific milestone -61. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone -62. `promote_milestone` - Promote a milestone to the next stage -63. `get_milestone_burndown_events` - Get burndown events for a specific milestone +12. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project +13. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided) +14. `create_note` - Create a new note (comment) to an issue or merge request +15. `create_merge_request_thread` - Create a new thread on a merge request +16. `mr_discussions` - List discussion items for a merge request +17. `update_merge_request_note` - Modify an existing merge request thread note +18. `create_merge_request_note` - Add a new note to an existing merge request thread +19. `update_issue_note` - Modify an existing issue thread note +20. `create_issue_note` - Add a new note to an existing issue thread +21. `list_issues` - List issues in a GitLab project with filtering options +22. `get_issue` - Get details of a specific issue in a GitLab project +23. `update_issue` - Update an issue in a GitLab project +24. `delete_issue` - Delete an issue from a GitLab project +25. `list_issue_links` - List all issue links for a specific issue +26. `list_issue_discussions` - List discussions for an issue in a GitLab project +27. `get_issue_link` - Get a specific issue link +28. `create_issue_link` - Create an issue link between two issues +29. `delete_issue_link` - Delete an issue link +30. `list_namespaces` - List all namespaces available to the current user +31. `get_namespace` - Get details of a namespace by ID or path +32. `verify_namespace` - Verify if a namespace path exists +33. `get_project` - Get details of a specific project +34. `list_projects` - List projects accessible by the current user +35. `list_labels` - List labels for a project +36. `get_label` - Get a single label from a project +37. `create_label` - Create a new label in a project +38. `update_label` - Update an existing label in a project +39. `delete_label` - Delete a label from a project +40. `list_group_projects` - List projects in a GitLab group with filtering options +41. `list_wiki_pages` - List wiki pages in a GitLab project +42. `get_wiki_page` - Get details of a specific wiki page +43. `create_wiki_page` - Create a new wiki page in a GitLab project +44. `update_wiki_page` - Update an existing wiki page in a GitLab project +45. `delete_wiki_page` - Delete a wiki page from a GitLab project +46. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories) +47. `list_pipelines` - List pipelines in a GitLab project with filtering options +48. `get_pipeline` - Get details of a specific pipeline in a GitLab project +49. `list_pipeline_jobs` - List all jobs in a specific pipeline +50. `get_pipeline_job` - Get details of a GitLab pipeline job number +51. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number +52. `create_pipeline` - Create a new pipeline for a branch or tag +53. `retry_pipeline` - Retry a failed or canceled pipeline +54. `cancel_pipeline` - Cancel a running pipeline +55. `list_merge_requests` - List merge requests in a GitLab project with filtering options +56. `list_milestones` - List milestones in a GitLab project with filtering options +57. `get_milestone` - Get details of a specific milestone +58. `create_milestone` - Create a new milestone in a GitLab project +59. `edit_milestone` - Edit an existing milestone in a GitLab project +60. `delete_milestone` - Delete a milestone from a GitLab project +61. `get_milestone_issue` - Get issues associated with a specific milestone +62. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone +63. `promote_milestone` - Promote a milestone to the next stage +64. `get_milestone_burndown_events` - Get burndown events for a specific milestone +65. `get_users` - Get GitLab user details by usernames diff --git a/index.ts b/index.ts index 6f2a4b4..c6a1afc 100644 --- a/index.ts +++ b/index.ts @@ -34,6 +34,9 @@ import { GitLabNamespaceExistsResponseSchema, GitLabProjectSchema, GitLabLabelSchema, + GitLabUserSchema, + GitLabUsersResponseSchema, + GetUsersSchema, CreateRepositoryOptionsSchema, CreateIssueOptionsSchema, CreateMergeRequestOptionsSchema, @@ -47,7 +50,7 @@ import { CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, - GitLabMergeRequestDiffSchema, + GitLabDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, @@ -117,6 +120,8 @@ import { type GitLabNamespaceExistsResponse, type GitLabProject, type GitLabLabel, + type GitLabUser, + type GitLabUsersResponse, type GitLabPipeline, type ListPipelinesOptions, type GetPipelineOptions, @@ -159,6 +164,9 @@ import { GetMilestoneMergeRequestsSchema, PromoteProjectMilestoneSchema, GetMilestoneBurndownEventsSchema, + GitLabCompareResult, + GitLabCompareResultSchema, + GetBranchDiffsSchema, } from "./schemas.js"; /** @@ -307,6 +315,12 @@ const allTools = [ "Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)", inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema), }, + { + name: "get_branch_diffs", + description: + "Get the changes/diffs between two branches or commits in a GitLab project", + inputSchema: zodToJsonSchema(GetBranchDiffsSchema), + }, { name: "update_merge_request", description: "Update a merge request (Either mergeRequestIid or branchName must be provided)", @@ -567,6 +581,11 @@ const allTools = [ description: "Get burndown events for a specific milestone", inputSchema: zodToJsonSchema(GetMilestoneBurndownEventsSchema), }, + { + name: "get_users", + description: "Get GitLab user details by usernames", + inputSchema: zodToJsonSchema(GetUsersSchema), + }, ]; // Define which tools are read-only @@ -575,6 +594,7 @@ const readOnlyTools = [ "get_file_contents", "get_merge_request", "get_merge_request_diffs", + "get_branch_diffs", "mr_discussions", "list_issues", "list_merge_requests", @@ -603,6 +623,7 @@ const readOnlyTools = [ "get_milestone_burndown_events", "list_wiki_pages", "get_wiki_page", + "get_users", ]; // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI @@ -1161,6 +1182,9 @@ async function createMergeRequest( description: options.description, source_branch: options.source_branch, target_branch: options.target_branch, + assignee_ids: options.assignee_ids, + reviewer_ids: options.reviewer_ids, + labels: options.labels?.join(","), allow_collaboration: options.allow_collaboration, draft: options.draft, }), @@ -1773,7 +1797,50 @@ async function getMergeRequestDiffs( await handleGitLabError(response); const data = (await response.json()) as { changes: unknown }; - return z.array(GitLabMergeRequestDiffSchema).parse(data.changes); + return z.array(GitLabDiffSchema).parse(data.changes); +} + +/** + * Get branch comparison diffs + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {string} from - The branch name or commit SHA to compare from + * @param {string} to - The branch name or commit SHA to compare to + * @param {boolean} [straight] - Comparison method: false for '...' (default), true for '--' + * @returns {Promise} Branch comparison results + */ +async function getBranchDiffs( + projectId: string, + from: string, + to: string, + straight?: boolean +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/compare` + ); + + url.searchParams.append("from", from); + url.searchParams.append("to", to); + + if (straight !== undefined) { + url.searchParams.append("straight", straight.toString()); + } + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error( + `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}` + ); + } + + const data = await response.json(); + return GitLabCompareResultSchema.parse(data); } /** @@ -2868,6 +2935,65 @@ async function getMilestoneBurndownEvents(projectId: string, milestoneId: number return data as any[]; } +/** + * Get a single user from GitLab + * + * @param {string} username - The username to look up + * @returns {Promise} The user data or null if not found + */ +async function getUser(username: string): Promise { + try { + const url = new URL(`${GITLAB_API_URL}/users`); + url.searchParams.append("username", username); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + await handleGitLabError(response); + + const users = await response.json(); + + // GitLab returns an array of users that match the username + if (Array.isArray(users) && users.length > 0) { + // Find exact match for username (case-sensitive) + const exactMatch = users.find(user => user.username === username); + if (exactMatch) { + return GitLabUserSchema.parse(exactMatch); + } + } + + // No matching user found + return null; + } catch (error) { + console.error(`Error fetching user by username '${username}':`, error); + return null; + } +} + +/** + * Get multiple users from GitLab + * + * @param {string[]} usernames - Array of usernames to look up + * @returns {Promise} Object with usernames as keys and user objects or null as values + */ +async function getUsers(usernames: string[]): Promise { + const users: Record = {}; + + // Process usernames sequentially to avoid rate limiting + for (const username of usernames) { + try { + const user = await getUser(username); + users[username] = user; + } catch (error) { + console.error(`Error processing username '${username}':`, error); + users[username] = null; + } + } + + return GitLabUsersResponseSchema.parse(users); +} + server.setRequestHandler(ListToolsRequestSchema, async () => { // Apply read-only filter first const tools0 = GITLAB_READ_ONLY_MODE @@ -2956,6 +3082,34 @@ server.setRequestHandler(CallToolRequestSchema, async request => { }; } + case "get_branch_diffs": { + const args = GetBranchDiffsSchema.parse(request.params.arguments); + const diffResp = await getBranchDiffs( + args.project_id, + args.from, + args.to, + args.straight + ); + + if (args.excluded_file_patterns?.length) { + const regexPatterns = args.excluded_file_patterns.map(pattern => new RegExp(pattern)); + + // Helper function to check if a path matches any regex pattern + const matchesAnyPattern = (path: string): boolean => { + if (!path) return false; + return regexPatterns.some(regex => regex.test(path)); + }; + + // Filter out files that match any of the regex patterns on new files + diffResp.diffs = diffResp.diffs.filter(diff => + !matchesAnyPattern(diff.new_path) + ); + } + return { + content: [{ type: "text", text: JSON.stringify(diffResp, null, 2) }], + }; + } + case "search_repositories": { const args = SearchRepositoriesSchema.parse(request.params.arguments); const results = await searchProjects(args.search, args.page, args.per_page); @@ -3226,6 +3380,15 @@ server.setRequestHandler(CallToolRequestSchema, async request => { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }], }; } + + case "get_users": { + const args = GetUsersSchema.parse(request.params.arguments); + const usersMap = await getUsers(args.usernames); + + return { + content: [{ type: "text", text: JSON.stringify(usersMap, null, 2) }], + }; + } case "create_note": { const args = CreateNoteSchema.parse(request.params.arguments); diff --git a/schemas.ts b/schemas.ts index 67b5e1e..85b17c7 100644 --- a/schemas.ts +++ b/schemas.ts @@ -190,11 +190,35 @@ export const GetPipelineJobOutputSchema = z.object({ job_id: z.number().describe("The ID of the job"), }); +// User schemas +export const GitLabUserSchema = z.object({ + username: z.string(), // Changed from login to match GitLab API + id: z.number(), + name: z.string(), + avatar_url: z.string().nullable(), + web_url: z.string(), // Changed from html_url to match GitLab API +}); + +export const GetUsersSchema = z.object({ + usernames: z.array(z.string()).describe("Array of usernames to search for"), +}); + +export const GitLabUsersResponseSchema = z.record( + z.string(), + z.object({ + id: z.number(), + username: z.string(), + name: z.string(), + avatar_url: z.string(), + web_url: z.string(), + }).nullable() +); + // Namespace related schemas // Base schema for project-related operations const ProjectParamsSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API + project_id: z.string().describe("Project ID or complete URL-encoded path to project"), // Changed from owner/repo to match GitLab API }); export const GitLabNamespaceSchema = z.object({ id: z.number(), @@ -286,6 +310,7 @@ export const GitLabRepositorySchema = z.object({ container_registry_access_level: z.string().optional(), issues_enabled: z.boolean().optional(), merge_requests_enabled: z.boolean().optional(), + merge_requests_template: z.string().optional(), wiki_enabled: z.boolean().optional(), jobs_enabled: z.boolean().optional(), snippets_enabled: z.boolean().optional(), @@ -430,13 +455,26 @@ export const CreateMergeRequestOptionsSchema = z.object({ description: z.string().optional(), // Changed from body to match GitLab API source_branch: z.string(), // Changed from head to match GitLab API target_branch: z.string(), // Changed from base to match GitLab API + assignee_ids: z + .array(z.number()) + .optional(), + reviewer_ids: z + .array(z.number()) + .optional(), + labels: z.array(z.string()).optional(), allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API draft: z.boolean().optional(), }); -export const CreateBranchOptionsSchema = z.object({ - name: z.string(), // Changed from ref to match GitLab API - ref: z.string(), // The source branch/commit for the new branch +export const GitLabDiffSchema = z.object({ + old_path: z.string(), + new_path: z.string(), + a_mode: z.string(), + b_mode: z.string(), + diff: z.string(), + new_file: z.boolean(), + renamed_file: z.boolean(), + deleted_file: z.boolean(), }); // Response schemas for operations @@ -454,6 +492,27 @@ export const GitLabSearchResponseSchema = z.object({ items: z.array(GitLabRepositorySchema), }); +// create branch schemas +export const CreateBranchOptionsSchema = z.object({ + name: z.string(), // Changed from ref to match GitLab API + ref: z.string(), // The source branch/commit for the new branch +}); + +export const GitLabCompareResultSchema = z.object({ + commit: z.object({ + id: z.string().optional(), + short_id: z.string().optional(), + title: z.string().optional(), + author_name: z.string().optional(), + author_email: z.string().optional(), + created_at: z.string().optional(), + }).optional(), + commits: z.array(GitLabCommitSchema), + diffs: z.array(GitLabDiffSchema), + compare_timeout: z.boolean().optional(), + compare_same_ref: z.boolean().optional(), +}); + // Issue related schemas export const GitLabLabelSchema = z.object({ id: z.number(), @@ -470,14 +529,6 @@ export const GitLabLabelSchema = z.object({ is_project_label: z.boolean().optional(), }); -export const GitLabUserSchema = z.object({ - username: z.string(), // Changed from login to match GitLab API - id: z.number(), - name: z.string(), - avatar_url: z.string().nullable(), - web_url: z.string(), // Changed from html_url to match GitLab API -}); - export const GitLabMilestoneSchema = z.object({ id: z.number(), iid: z.number(), // Added to match GitLab API @@ -567,6 +618,7 @@ export const GitLabMergeRequestSchema = z.object({ draft: z.boolean().optional(), author: GitLabUserSchema, assignees: z.array(GitLabUserSchema).optional(), + reviewers: z.array(GitLabUserSchema).optional(), source_branch: z.string(), target_branch: z.string(), diff_refs: GitLabMergeRequestDiffRefSchema.nullable().optional(), @@ -756,28 +808,39 @@ export const CreateMergeRequestSchema = ProjectParamsSchema.extend({ description: z.string().optional().describe("Merge request description"), source_branch: z.string().describe("Branch containing changes"), target_branch: z.string().describe("Branch to merge into"), + assignee_ids: z + .array(z.number()) + .optional() + .describe("The ID of the users to assign the MR to"), + reviewer_ids: z + .array(z.number()) + .optional() + .describe("The ID of the users to assign as reviewers of the MR"), + labels: z.array(z.string()).optional().describe("Labels for the MR"), draft: z.boolean().optional().describe("Create as draft merge request"), - allow_collaboration: z.boolean().optional().describe("Allow commits from upstream members"), + allow_collaboration: z + .boolean() + .optional() + .describe("Allow commits from upstream members"), }); export const ForkRepositorySchema = ProjectParamsSchema.extend({ namespace: z.string().optional().describe("Namespace to fork to (full path)"), }); +// Branch related schemas export const CreateBranchSchema = ProjectParamsSchema.extend({ branch: z.string().describe("Name for the new branch"), ref: z.string().optional().describe("Source branch/commit for new branch"), }); -export const GitLabMergeRequestDiffSchema = z.object({ - old_path: z.string(), - new_path: z.string(), - a_mode: z.string(), - b_mode: z.string(), - diff: z.string(), - new_file: z.boolean(), - renamed_file: z.boolean(), - deleted_file: z.boolean(), +export const GetBranchDiffsSchema = ProjectParamsSchema.extend({ + from: z.string().describe("The base branch or commit SHA to compare from"), + to: z.string().describe("The target branch or commit SHA to compare to"), + straight: z.boolean().optional().describe("Comparison method: false for '...' (default), true for '--'"), + excluded_file_patterns: z.array(z.string()).optional().describe( + "Array of regex patterns to exclude files from the diff results. Each pattern is a JavaScript-compatible regular expression that matches file paths to ignore. Examples: [\"^test/mocks/\", \"\\.spec\\.ts$\", \"package-lock\\.json\"]" + ), }); export const GetMergeRequestSchema = ProjectParamsSchema.extend({ @@ -1274,6 +1337,7 @@ export type GitLabDirectoryContent = z.infer; export type FileOperation = z.infer; export type GitLabTree = z.infer; +export type GitLabCompareResult = z.infer; export type GitLabCommit = z.infer; export type GitLabReference = z.infer; export type CreateRepositoryOptions = z.infer; @@ -1282,7 +1346,9 @@ export type CreateMergeRequestOptions = z.infer; export type GitLabCreateUpdateFileResponse = z.infer; export type GitLabSearchResponse = z.infer; -export type GitLabMergeRequestDiff = z.infer; +export type GitLabMergeRequestDiff = z.infer< + typeof GitLabDiffSchema +>; export type CreateNoteOptions = z.infer; export type GitLabIssueLink = z.infer; export type ListIssueDiscussionsOptions = z.infer; @@ -1321,3 +1387,5 @@ export type GetMilestoneIssuesOptions = z.infer export type GetMilestoneMergeRequestsOptions = z.infer; export type PromoteProjectMilestoneOptions = z.infer; export type GetMilestoneBurndownEventsOptions = z.infer; +export type GitLabUser = z.infer; +export type GitLabUsersResponse = z.infer;