@zereight/mcp-gitlab 1.0.63 → 1.0.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,7 +36,9 @@ When using with the Claude App, you need to set up your API key and URLs directl
36
36
  ```
37
37
 
38
38
  #### Docker
39
+
39
40
  - stdio
41
+
40
42
  ```mcp.json
41
43
  {
42
44
  "mcpServers": {
@@ -74,6 +76,7 @@ When using with the Claude App, you need to set up your API key and URLs directl
74
76
  ```
75
77
 
76
78
  - sse
79
+
77
80
  ```shell
78
81
  docker run -i --rm \
79
82
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
@@ -111,10 +114,14 @@ $ sh scripts/image_push.sh docker_user_name
111
114
  - `USE_GITLAB_WIKI`: When set to 'true', enables the wiki-related tools (list_wiki_pages, get_wiki_page, create_wiki_page, update_wiki_page, delete_wiki_page). By default, wiki features are disabled.
112
115
  - `USE_MILESTONE`: When set to 'true', enables the milestone-related tools (list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events). By default, milestone features are disabled.
113
116
  - `USE_PIPELINE`: When set to 'true', enables the pipeline-related tools (list_pipelines, get_pipeline, list_pipeline_jobs, get_pipeline_job, get_pipeline_job_output, create_pipeline, retry_pipeline, cancel_pipeline). By default, pipeline features are disabled.
117
+ - `GITLAB_AUTH_COOKIE_PATH`: Path to an authentication cookie file for GitLab instances that require cookie-based authentication. When provided, the cookie will be included in all GitLab API requests.
118
+
119
+ [![Star History Chart](https://api.star-history.com/svg?repos=zereight/gitlab-mcp&type=Date)](https://www.star-history.com/#zereight/gitlab-mcp&Date)
114
120
 
115
121
  ## Tools 🛠️
116
122
 
117
123
  +<!-- TOOLS-START -->
124
+
118
125
  1. `create_or_update_file` - Create or update a single file in a GitLab project
119
126
  2. `search_repositories` - Search for GitLab projects
120
127
  3. `create_repository` - Create a new GitLab project
@@ -126,58 +133,59 @@ $ sh scripts/image_push.sh docker_user_name
126
133
  9. `create_branch` - Create a new branch in a GitLab project
127
134
  10. `get_merge_request` - Get details of a merge request (Either mergeRequestIid or branchName must be provided)
128
135
  11. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)
129
- 12. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project
130
- 13. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
131
- 14. `create_note` - Create a new note (comment) to an issue or merge request
132
- 15. `create_merge_request_thread` - Create a new thread on a merge request
133
- 16. `mr_discussions` - List discussion items for a merge request
134
- 17. `update_merge_request_note` - Modify an existing merge request thread note
135
- 18. `create_merge_request_note` - Add a new note to an existing merge request thread
136
- 19. `update_issue_note` - Modify an existing issue thread note
137
- 20. `create_issue_note` - Add a new note to an existing issue thread
138
- 21. `list_issues` - List issues in a GitLab project with filtering options
139
- 22. `get_issue` - Get details of a specific issue in a GitLab project
140
- 23. `update_issue` - Update an issue in a GitLab project
141
- 24. `delete_issue` - Delete an issue from a GitLab project
142
- 25. `list_issue_links` - List all issue links for a specific issue
143
- 26. `list_issue_discussions` - List discussions for an issue in a GitLab project
144
- 27. `get_issue_link` - Get a specific issue link
145
- 28. `create_issue_link` - Create an issue link between two issues
146
- 29. `delete_issue_link` - Delete an issue link
147
- 30. `list_namespaces` - List all namespaces available to the current user
148
- 31. `get_namespace` - Get details of a namespace by ID or path
149
- 32. `verify_namespace` - Verify if a namespace path exists
150
- 33. `get_project` - Get details of a specific project
151
- 34. `list_projects` - List projects accessible by the current user
152
- 35. `list_labels` - List labels for a project
153
- 36. `get_label` - Get a single label from a project
154
- 37. `create_label` - Create a new label in a project
155
- 38. `update_label` - Update an existing label in a project
156
- 39. `delete_label` - Delete a label from a project
157
- 40. `list_group_projects` - List projects in a GitLab group with filtering options
158
- 41. `list_wiki_pages` - List wiki pages in a GitLab project
159
- 42. `get_wiki_page` - Get details of a specific wiki page
160
- 43. `create_wiki_page` - Create a new wiki page in a GitLab project
161
- 44. `update_wiki_page` - Update an existing wiki page in a GitLab project
162
- 45. `delete_wiki_page` - Delete a wiki page from a GitLab project
163
- 46. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
164
- 47. `list_pipelines` - List pipelines in a GitLab project with filtering options
165
- 48. `get_pipeline` - Get details of a specific pipeline in a GitLab project
166
- 49. `list_pipeline_jobs` - List all jobs in a specific pipeline
167
- 50. `get_pipeline_job` - Get details of a GitLab pipeline job number
168
- 51. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number
169
- 52. `create_pipeline` - Create a new pipeline for a branch or tag
170
- 53. `retry_pipeline` - Retry a failed or canceled pipeline
171
- 54. `cancel_pipeline` - Cancel a running pipeline
172
- 55. `list_merge_requests` - List merge requests in a GitLab project with filtering options
173
- 56. `list_milestones` - List milestones in a GitLab project with filtering options
174
- 57. `get_milestone` - Get details of a specific milestone
175
- 58. `create_milestone` - Create a new milestone in a GitLab project
176
- 59. `edit_milestone` - Edit an existing milestone in a GitLab project
177
- 60. `delete_milestone` - Delete a milestone from a GitLab project
178
- 61. `get_milestone_issue` - Get issues associated with a specific milestone
179
- 62. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
180
- 63. `promote_milestone` - Promote a milestone to the next stage
181
- 64. `get_milestone_burndown_events` - Get burndown events for a specific milestone
182
- 65. `get_users` - Get GitLab user details by usernames
136
+ 12. `list_merge_request_diffs` - List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)
137
+ 13. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project
138
+ 14. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
139
+ 15. `create_note` - Create a new note (comment) to an issue or merge request
140
+ 16. `create_merge_request_thread` - Create a new thread on a merge request
141
+ 17. `mr_discussions` - List discussion items for a merge request
142
+ 18. `update_merge_request_note` - Modify an existing merge request thread note
143
+ 19. `create_merge_request_note` - Add a new note to an existing merge request thread
144
+ 20. `update_issue_note` - Modify an existing issue thread note
145
+ 21. `create_issue_note` - Add a new note to an existing issue thread
146
+ 22. `list_issues` - List issues in a GitLab project with filtering options
147
+ 23. `get_issue` - Get details of a specific issue in a GitLab project
148
+ 24. `update_issue` - Update an issue in a GitLab project
149
+ 25. `delete_issue` - Delete an issue from a GitLab project
150
+ 26. `list_issue_links` - List all issue links for a specific issue
151
+ 27. `list_issue_discussions` - List discussions for an issue in a GitLab project
152
+ 28. `get_issue_link` - Get a specific issue link
153
+ 29. `create_issue_link` - Create an issue link between two issues
154
+ 30. `delete_issue_link` - Delete an issue link
155
+ 31. `list_namespaces` - List all namespaces available to the current user
156
+ 32. `get_namespace` - Get details of a namespace by ID or path
157
+ 33. `verify_namespace` - Verify if a namespace path exists
158
+ 34. `get_project` - Get details of a specific project
159
+ 35. `list_projects` - List projects accessible by the current user
160
+ 36. `list_labels` - List labels for a project
161
+ 37. `get_label` - Get a single label from a project
162
+ 38. `create_label` - Create a new label in a project
163
+ 39. `update_label` - Update an existing label in a project
164
+ 40. `delete_label` - Delete a label from a project
165
+ 41. `list_group_projects` - List projects in a GitLab group with filtering options
166
+ 42. `list_wiki_pages` - List wiki pages in a GitLab project
167
+ 43. `get_wiki_page` - Get details of a specific wiki page
168
+ 44. `create_wiki_page` - Create a new wiki page in a GitLab project
169
+ 45. `update_wiki_page` - Update an existing wiki page in a GitLab project
170
+ 46. `delete_wiki_page` - Delete a wiki page from a GitLab project
171
+ 47. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
172
+ 48. `list_pipelines` - List pipelines in a GitLab project with filtering options
173
+ 49. `get_pipeline` - Get details of a specific pipeline in a GitLab project
174
+ 50. `list_pipeline_jobs` - List all jobs in a specific pipeline
175
+ 51. `get_pipeline_job` - Get details of a GitLab pipeline job number
176
+ 52. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number
177
+ 53. `create_pipeline` - Create a new pipeline for a branch or tag
178
+ 54. `retry_pipeline` - Retry a failed or canceled pipeline
179
+ 55. `cancel_pipeline` - Cancel a running pipeline
180
+ 56. `list_merge_requests` - List merge requests in a GitLab project with filtering options
181
+ 57. `list_milestones` - List milestones in a GitLab project with filtering options
182
+ 58. `get_milestone` - Get details of a specific milestone
183
+ 59. `create_milestone` - Create a new milestone in a GitLab project
184
+ 60. `edit_milestone` - Edit an existing milestone in a GitLab project
185
+ 61. `delete_milestone` - Delete a milestone from a GitLab project
186
+ 62. `get_milestone_issue` - Get issues associated with a specific milestone
187
+ 63. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
188
+ 64. `promote_milestone` - Promote a milestone to the next stage
189
+ 65. `get_milestone_burndown_events` - Get burndown events for a specific milestone
190
+ 66. `get_users` - Get GitLab user details by usernames
183
191
  <!-- TOOLS-END -->
package/build/index.js CHANGED
@@ -3,7 +3,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
- import fetch from "node-fetch";
6
+ import nodeFetch from "node-fetch";
7
+ import fetchCookie from "fetch-cookie";
8
+ import { CookieJar, parse as parseCookie } from "tough-cookie";
7
9
  import { SocksProxyAgent } from "socks-proxy-agent";
8
10
  import { HttpsProxyAgent } from "https-proxy-agent";
9
11
  import { HttpProxyAgent } from "http-proxy-agent";
@@ -25,7 +27,7 @@ GetPipelineJobOutputSchema, GitLabPipelineJobSchema,
25
27
  GitLabDiscussionNoteSchema, // Added
26
28
  GitLabDiscussionSchema, PaginatedDiscussionsResponseSchema, UpdateMergeRequestNoteSchema, // Added
27
29
  CreateMergeRequestNoteSchema, // Added
28
- ListMergeRequestDiscussionsSchema, UpdateIssueNoteSchema, CreateIssueNoteSchema, ListMergeRequestsSchema, GitLabMilestonesSchema, ListProjectMilestonesSchema, GetProjectMilestoneSchema, CreateProjectMilestoneSchema, EditProjectMilestoneSchema, DeleteProjectMilestoneSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, PromoteProjectMilestoneSchema, GetMilestoneBurndownEventsSchema, GitLabCompareResultSchema, GetBranchDiffsSchema, } from "./schemas.js";
30
+ ListMergeRequestDiscussionsSchema, UpdateIssueNoteSchema, CreateIssueNoteSchema, ListMergeRequestsSchema, GitLabMilestonesSchema, ListProjectMilestonesSchema, GetProjectMilestoneSchema, CreateProjectMilestoneSchema, EditProjectMilestoneSchema, DeleteProjectMilestoneSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, PromoteProjectMilestoneSchema, GetMilestoneBurndownEventsSchema, GitLabCompareResultSchema, GetBranchDiffsSchema, ListCommitsSchema, GetCommitSchema, GetCommitDiffSchema, ListMergeRequestDiffsSchema, } from "./schemas.js";
29
31
  /**
30
32
  * Read version from package.json
31
33
  */
@@ -51,6 +53,7 @@ const server = new Server({
51
53
  },
52
54
  });
53
55
  const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
56
+ const GITLAB_AUTH_COOKIE_PATH = process.env.GITLAB_AUTH_COOKIE_PATH;
54
57
  const IS_OLD = process.env.GITLAB_IS_OLD === "true";
55
58
  const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
56
59
  const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true";
@@ -91,6 +94,76 @@ if (HTTPS_PROXY) {
91
94
  }
92
95
  httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
93
96
  httpAgent = httpAgent || new Agent();
97
+ // Create cookie jar with clean Netscape file parsing
98
+ const createCookieJar = () => {
99
+ if (!GITLAB_AUTH_COOKIE_PATH)
100
+ return null;
101
+ try {
102
+ const cookiePath = GITLAB_AUTH_COOKIE_PATH.startsWith("~/")
103
+ ? path.join(process.env.HOME || "", GITLAB_AUTH_COOKIE_PATH.slice(2))
104
+ : GITLAB_AUTH_COOKIE_PATH;
105
+ const jar = new CookieJar();
106
+ const cookieContent = fs.readFileSync(cookiePath, "utf8");
107
+ cookieContent.split("\n").forEach(line => {
108
+ // Handle #HttpOnly_ prefix
109
+ if (line.startsWith("#HttpOnly_")) {
110
+ line = line.slice(10);
111
+ }
112
+ // Skip comments and empty lines
113
+ if (line.startsWith("#") || !line.trim()) {
114
+ return;
115
+ }
116
+ // Parse Netscape format: domain, flag, path, secure, expires, name, value
117
+ const parts = line.split("\t");
118
+ if (parts.length >= 7) {
119
+ const [domain, , path, secure, expires, name, value] = parts;
120
+ // Build cookie string in standard format
121
+ const cookieStr = `${name}=${value}; Domain=${domain}; Path=${path}${secure === "TRUE" ? "; Secure" : ""}${expires !== "0" ? `; Expires=${new Date(parseInt(expires) * 1000).toUTCString()}` : ""}`;
122
+ // Use tough-cookie's parse function for robust parsing
123
+ const cookie = parseCookie(cookieStr);
124
+ if (cookie) {
125
+ const url = `${secure === "TRUE" ? "https" : "http"}://${domain.startsWith(".") ? domain.slice(1) : domain}`;
126
+ jar.setCookieSync(cookie, url);
127
+ }
128
+ }
129
+ });
130
+ return jar;
131
+ }
132
+ catch (error) {
133
+ console.error("Error loading cookie file:", error);
134
+ return null;
135
+ }
136
+ };
137
+ // Initialize cookie jar and fetch
138
+ const cookieJar = createCookieJar();
139
+ const fetch = cookieJar ? fetchCookie(nodeFetch, cookieJar) : nodeFetch;
140
+ // Ensure session is established for the current request
141
+ async function ensureSessionForRequest() {
142
+ if (!cookieJar || !GITLAB_AUTH_COOKIE_PATH)
143
+ return;
144
+ // Extract the base URL from GITLAB_API_URL
145
+ const apiUrl = new URL(GITLAB_API_URL);
146
+ const baseUrl = `${apiUrl.protocol}//${apiUrl.hostname}`;
147
+ // Check if we already have GitLab session cookies
148
+ const gitlabCookies = cookieJar.getCookiesSync(baseUrl);
149
+ const hasSessionCookie = gitlabCookies.some(cookie => cookie.key === '_gitlab_session' || cookie.key === 'remember_user_token');
150
+ if (!hasSessionCookie) {
151
+ try {
152
+ // Establish session with a lightweight request
153
+ await fetch(`${GITLAB_API_URL}/user`, {
154
+ ...DEFAULT_FETCH_CONFIG,
155
+ redirect: 'follow'
156
+ }).catch(() => {
157
+ // Ignore errors - the important thing is that cookies get set during redirects
158
+ });
159
+ // Small delay to ensure cookies are fully processed
160
+ await new Promise(resolve => setTimeout(resolve, 100));
161
+ }
162
+ catch (error) {
163
+ // Ignore session establishment errors
164
+ }
165
+ }
166
+ }
94
167
  // Modify DEFAULT_HEADERS to include agent configuration
95
168
  const DEFAULT_HEADERS = {
96
169
  Accept: "application/json",
@@ -169,6 +242,11 @@ const allTools = [
169
242
  description: "Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)",
170
243
  inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
171
244
  },
245
+ {
246
+ name: "list_merge_request_diffs",
247
+ description: "List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)",
248
+ inputSchema: zodToJsonSchema(ListMergeRequestDiffsSchema),
249
+ },
172
250
  {
173
251
  name: "get_branch_diffs",
174
252
  description: "Get the changes/diffs between two branches or commits in a GitLab project",
@@ -439,6 +517,21 @@ const allTools = [
439
517
  description: "Get GitLab user details by usernames",
440
518
  inputSchema: zodToJsonSchema(GetUsersSchema),
441
519
  },
520
+ {
521
+ name: "list_commits",
522
+ description: "List repository commits with filtering options",
523
+ inputSchema: zodToJsonSchema(ListCommitsSchema),
524
+ },
525
+ {
526
+ name: "get_commit",
527
+ description: "Get details of a specific commit",
528
+ inputSchema: zodToJsonSchema(GetCommitSchema),
529
+ },
530
+ {
531
+ name: "get_commit_diff",
532
+ description: "Get changes/diffs of a specific commit",
533
+ inputSchema: zodToJsonSchema(GetCommitDiffSchema),
534
+ },
442
535
  ];
443
536
  // Define which tools are read-only
444
537
  const readOnlyTools = [
@@ -476,6 +569,9 @@ const readOnlyTools = [
476
569
  "list_wiki_pages",
477
570
  "get_wiki_page",
478
571
  "get_users",
572
+ "list_commits",
573
+ "get_commit",
574
+ "get_commit_diff",
479
575
  ];
480
576
  // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
481
577
  const wikiToolNames = [
@@ -1383,6 +1479,41 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view
1383
1479
  const data = (await response.json());
1384
1480
  return z.array(GitLabDiffSchema).parse(data.changes);
1385
1481
  }
1482
+ /**
1483
+ * Get merge request changes with detailed information including commits, diff_refs, and more
1484
+ * 마지막으로 추가된 상세한 MR 변경사항 조회 함수 (Detailed merge request changes retrieval function)
1485
+ *
1486
+ * @param {string} projectId - The ID or URL-encoded path of the project
1487
+ * @param {number} mergeRequestIid - The internal ID of the merge request (Either mergeRequestIid or branchName must be provided)
1488
+ * @param {string} [branchName] - The name of the branch to search for merge request by branch name (Either mergeRequestIid or branchName must be provided)
1489
+ * @param {boolean} [unidiff] - Return diff in unidiff format
1490
+ * @returns {Promise<any>} The complete merge request changes response
1491
+ */
1492
+ async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, page, perPage, unidiff) {
1493
+ projectId = decodeURIComponent(projectId); // Decode project ID
1494
+ if (!mergeRequestIid && !branchName) {
1495
+ throw new Error("Either mergeRequestIid or branchName must be provided");
1496
+ }
1497
+ if (branchName && !mergeRequestIid) {
1498
+ const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
1499
+ mergeRequestIid = mergeRequest.iid;
1500
+ }
1501
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/diffs`);
1502
+ if (page) {
1503
+ url.searchParams.append("page", page.toString());
1504
+ }
1505
+ if (perPage) {
1506
+ url.searchParams.append("per_page", perPage.toString());
1507
+ }
1508
+ if (unidiff) {
1509
+ url.searchParams.append("unidiff", "true");
1510
+ }
1511
+ const response = await fetch(url.toString(), {
1512
+ ...DEFAULT_FETCH_CONFIG,
1513
+ });
1514
+ await handleGitLabError(response);
1515
+ return await response.json(); // Return full response including commits, diff_refs, changes, etc.
1516
+ }
1386
1517
  /**
1387
1518
  * Get branch comparison diffs
1388
1519
  *
@@ -2322,6 +2453,89 @@ async function getUsers(usernames) {
2322
2453
  }
2323
2454
  return GitLabUsersResponseSchema.parse(users);
2324
2455
  }
2456
+ /**
2457
+ * List repository commits
2458
+ * 저장소 커밋 목록 조회
2459
+ *
2460
+ * @param {string} projectId - Project ID or URL-encoded path
2461
+ * @param {ListCommitsOptions} options - List commits options
2462
+ * @returns {Promise<GitLabCommit[]>} List of commits
2463
+ */
2464
+ async function listCommits(projectId, options = {}) {
2465
+ projectId = decodeURIComponent(projectId);
2466
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`);
2467
+ // Add query parameters
2468
+ if (options.ref_name)
2469
+ url.searchParams.append("ref_name", options.ref_name);
2470
+ if (options.since)
2471
+ url.searchParams.append("since", options.since);
2472
+ if (options.until)
2473
+ url.searchParams.append("until", options.until);
2474
+ if (options.path)
2475
+ url.searchParams.append("path", options.path);
2476
+ if (options.author)
2477
+ url.searchParams.append("author", options.author);
2478
+ if (options.all)
2479
+ url.searchParams.append("all", options.all.toString());
2480
+ if (options.with_stats)
2481
+ url.searchParams.append("with_stats", options.with_stats.toString());
2482
+ if (options.first_parent)
2483
+ url.searchParams.append("first_parent", options.first_parent.toString());
2484
+ if (options.order)
2485
+ url.searchParams.append("order", options.order);
2486
+ if (options.trailers)
2487
+ url.searchParams.append("trailers", options.trailers.toString());
2488
+ if (options.page)
2489
+ url.searchParams.append("page", options.page.toString());
2490
+ if (options.per_page)
2491
+ url.searchParams.append("per_page", options.per_page.toString());
2492
+ const response = await fetch(url.toString(), {
2493
+ ...DEFAULT_FETCH_CONFIG,
2494
+ });
2495
+ await handleGitLabError(response);
2496
+ const data = await response.json();
2497
+ return z.array(GitLabCommitSchema).parse(data);
2498
+ }
2499
+ /**
2500
+ * Get a single commit
2501
+ * 단일 커밋 정보 조회
2502
+ *
2503
+ * @param {string} projectId - Project ID or URL-encoded path
2504
+ * @param {string} sha - The commit hash or name of a repository branch or tag
2505
+ * @param {boolean} [stats] - Include commit stats
2506
+ * @returns {Promise<GitLabCommit>} The commit details
2507
+ */
2508
+ async function getCommit(projectId, sha, stats) {
2509
+ projectId = decodeURIComponent(projectId);
2510
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits/${encodeURIComponent(sha)}`);
2511
+ if (stats) {
2512
+ url.searchParams.append("stats", "true");
2513
+ }
2514
+ const response = await fetch(url.toString(), {
2515
+ ...DEFAULT_FETCH_CONFIG,
2516
+ });
2517
+ await handleGitLabError(response);
2518
+ const data = await response.json();
2519
+ return GitLabCommitSchema.parse(data);
2520
+ }
2521
+ /**
2522
+ * Get commit diff
2523
+ * 커밋 변경사항 조회
2524
+ *
2525
+ * @param {string} projectId - Project ID or URL-encoded path
2526
+ * @param {string} sha - The commit hash or name of a repository branch or tag
2527
+ * @returns {Promise<GitLabMergeRequestDiff[]>} The commit diffs
2528
+ */
2529
+ async function getCommitDiff(projectId, sha) {
2530
+ projectId = decodeURIComponent(projectId);
2531
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits/${encodeURIComponent(sha)}/diff`);
2532
+ const response = await fetch(url.toString(), {
2533
+ ...DEFAULT_FETCH_CONFIG,
2534
+ });
2535
+ await handleGitLabError(response);
2536
+ const data = await response.json();
2537
+ return z.array(GitLabDiffSchema).parse(data);
2538
+ }
2325
2539
  server.setRequestHandler(ListToolsRequestSchema, async () => {
2326
2540
  // Apply read-only filter first
2327
2541
  const tools0 = GITLAB_READ_ONLY_MODE
@@ -2362,6 +2576,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2362
2576
  if (!request.params.arguments) {
2363
2577
  throw new Error("Arguments are required");
2364
2578
  }
2579
+ // Ensure session is established for every request if cookie authentication is enabled
2580
+ if (GITLAB_AUTH_COOKIE_PATH) {
2581
+ await ensureSessionForRequest();
2582
+ }
2365
2583
  switch (request.params.name) {
2366
2584
  case "fork_repository": {
2367
2585
  const forkArgs = ForkRepositorySchema.parse(request.params.arguments);
@@ -2514,6 +2732,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2514
2732
  content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
2515
2733
  };
2516
2734
  }
2735
+ case "list_merge_request_diffs": {
2736
+ const args = ListMergeRequestDiffsSchema.parse(request.params.arguments);
2737
+ const changes = await listMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.page, args.per_page, args.unidiff);
2738
+ return {
2739
+ content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
2740
+ };
2741
+ }
2517
2742
  case "update_merge_request": {
2518
2743
  const args = UpdateMergeRequestSchema.parse(request.params.arguments);
2519
2744
  const { project_id, merge_request_iid, source_branch, ...options } = args;
@@ -3011,6 +3236,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3011
3236
  ],
3012
3237
  };
3013
3238
  }
3239
+ case "list_commits": {
3240
+ const args = ListCommitsSchema.parse(request.params.arguments);
3241
+ const commits = await listCommits(args.project_id, args);
3242
+ return {
3243
+ content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
3244
+ };
3245
+ }
3246
+ case "get_commit": {
3247
+ const args = GetCommitSchema.parse(request.params.arguments);
3248
+ const commit = await getCommit(args.project_id, args.sha, args.stats);
3249
+ return {
3250
+ content: [{ type: "text", text: JSON.stringify(commit, null, 2) }],
3251
+ };
3252
+ }
3253
+ case "get_commit_diff": {
3254
+ const args = GetCommitDiffSchema.parse(request.params.arguments);
3255
+ const diff = await getCommitDiff(args.project_id, args.sha);
3256
+ return {
3257
+ content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
3258
+ };
3259
+ }
3014
3260
  default:
3015
3261
  throw new Error(`Unknown tool: ${request.params.name}`);
3016
3262
  }
package/build/schemas.js CHANGED
@@ -274,14 +274,14 @@ export const GitLabRepositorySchema = z.object({
274
274
  project_access: z
275
275
  .object({
276
276
  access_level: z.number(),
277
- notification_level: z.number().optional(),
277
+ notification_level: z.number().nullable().optional(),
278
278
  })
279
279
  .optional()
280
280
  .nullable(),
281
281
  group_access: z
282
282
  .object({
283
283
  access_level: z.number(),
284
- notification_level: z.number().optional(),
284
+ notification_level: z.number().nullable().optional(),
285
285
  })
286
286
  .optional()
287
287
  .nullable(),
@@ -374,8 +374,17 @@ export const GitLabCommitSchema = z.object({
374
374
  committer_name: z.string(),
375
375
  committer_email: z.string(),
376
376
  committed_date: z.string(),
377
+ created_at: z.string().optional(), // Add created_at field
378
+ message: z.string().optional(), // Add full message field
377
379
  web_url: z.string(), // Changed from html_url to match GitLab API
378
380
  parent_ids: z.array(z.string()), // Changed from parents to match GitLab API
381
+ stats: z.object({
382
+ additions: z.number().optional().nullable(),
383
+ deletions: z.number().optional().nullable(),
384
+ total: z.number().optional().nullable(),
385
+ }).optional(), // Only present when with_stats=true
386
+ trailers: z.record(z.string()).optional().default({}), // Git trailers, may be empty object
387
+ extended_trailers: z.record(z.array(z.string())).optional().default({}), // Extended trailers, may be empty object
379
388
  });
380
389
  // Reference schema
381
390
  export const GitLabReferenceSchema = z.object({
@@ -596,6 +605,20 @@ export const GitLabMergeRequestSchema = z.object({
596
605
  squash: z.boolean().optional(),
597
606
  labels: z.array(z.string()).optional(),
598
607
  });
608
+ export const LineRangeSchema = z.object({
609
+ start: z.object({
610
+ line_code: z.string().nullable().optional().describe("CRITICAL: Line identifier in format '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. USUALLY REQUIRED for GitLab diff comments despite being optional in schema. Example: 'a1b2c3d4e5f6_10_15'. Get this from GitLab diff API response, never fabricate."),
611
+ type: z.enum(["new", "old"]).nullable().optional().describe("Line type: 'old' = deleted/original line, 'new' = added/modified line, null = unchanged context. MUST match the line_code format and old_line/new_line values."),
612
+ old_line: z.number().nullable().optional().describe("Line number in original file (before changes). REQUIRED when type='old', NULL when type='new' (for purely added lines), can be present for context lines."),
613
+ new_line: z.number().nullable().optional().describe("Line number in modified file (after changes). REQUIRED when type='new', NULL when type='old' (for purely deleted lines), can be present for context lines."),
614
+ }).describe("Start line position for multiline comment range. MUST specify either old_line OR new_line (or both for context), never neither."),
615
+ end: z.object({
616
+ line_code: z.string().nullable().optional().describe("CRITICAL: Line identifier in format '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. USUALLY REQUIRED for GitLab diff comments despite being optional in schema. Example: 'a1b2c3d4e5f6_12_17'. Must be from same file as start.line_code."),
617
+ type: z.enum(["new", "old"]).nullable().optional().describe("Line type: 'old' = deleted/original line, 'new' = added/modified line, null = unchanged context. SHOULD MATCH start.type for consistent ranges (don't mix old/new types)."),
618
+ old_line: z.number().nullable().optional().describe("Line number in original file (before changes). REQUIRED when type='old', NULL when type='new' (for purely added lines), can be present for context lines. MUST be >= start.old_line if both specified."),
619
+ new_line: z.number().nullable().optional().describe("Line number in modified file (after changes). REQUIRED when type='new', NULL when type='old' (for purely deleted lines), can be present for context lines. MUST be >= start.new_line if both specified."),
620
+ }).describe("End line position for multiline comment range. MUST specify either old_line OR new_line (or both for context), never neither. Range must be valid (end >= start)."),
621
+ }).describe("Line range for multiline comments on GitLab merge request diffs. VALIDATION RULES: 1) line_code is critical for GitLab API success, 2) start/end must have consistent types, 3) line numbers must form valid range, 4) get line_code from GitLab diff API, never generate manually.");
599
622
  // Discussion related schemas
600
623
  export const GitLabDiscussionNoteSchema = z.object({
601
624
  id: z.number(),
@@ -620,24 +643,24 @@ export const GitLabDiscussionNoteSchema = z.object({
620
643
  base_sha: z.string(),
621
644
  start_sha: z.string(),
622
645
  head_sha: z.string(),
623
- old_path: z.string(),
624
- new_path: z.string(),
646
+ old_path: z.string().optional().describe("File path before change"),
647
+ new_path: z.string().optional().describe("File path after change"),
625
648
  position_type: z.enum(["text", "image", "file"]),
626
- old_line: z.number().nullish(), // This is missing for image diffs
627
- new_line: z.number().nullish(), // This is missing for image diffs
649
+ new_line: z.number().nullable().optional().describe("Line number in the modified file (after changes). Used for added lines and context lines. Null for deleted lines."),
650
+ old_line: z.number().nullable().optional().describe("Line number in the original file (before changes). Used for deleted lines and context lines. Null for newly added lines."),
628
651
  line_range: z
629
652
  .object({
630
653
  start: z.object({
631
- line_code: z.string(),
654
+ line_code: z.string().nullable().optional().describe("Line identifier in format: '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. Used to uniquely identify a specific line in the diff."),
632
655
  type: z.enum(["new", "old", "expanded"]),
633
- old_line: z.number().nullish(), // This is missing for image diffs
634
- new_line: z.number().nullish(), // This is missing for image diffs
656
+ old_line: z.number().nullable().optional().describe("Line number in the original file (before changes). Null for newly added lines or unchanged context lines."),
657
+ new_line: z.number().nullable().optional().describe("Line number in the modified file (after changes). Null for deleted lines or unchanged context lines."),
635
658
  }),
636
659
  end: z.object({
637
- line_code: z.string(),
660
+ line_code: z.string().nullable().optional().describe("Line identifier in format: '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. Used to uniquely identify a specific line in the diff."),
638
661
  type: z.enum(["new", "old", "expanded"]),
639
- old_line: z.number().nullish(), // This is missing for image diffs
640
- new_line: z.number().nullish(), // This is missing for image diffs
662
+ old_line: z.number().nullable().optional().describe("Line number in the original file (before changes). Null for newly added lines or unchanged context lines."),
663
+ new_line: z.number().nullable().optional().describe("Line number in the modified file (after changes). Null for deleted lines or unchanged context lines."),
641
664
  }),
642
665
  })
643
666
  .nullable()
@@ -818,6 +841,11 @@ export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
818
841
  export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
819
842
  view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
820
843
  });
844
+ export const ListMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
845
+ page: z.number().optional().describe("Page number for pagination (default: 1)"),
846
+ per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"),
847
+ unidiff: z.boolean().optional().describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
848
+ });
821
849
  export const CreateNoteSchema = z.object({
822
850
  project_id: z.string().describe("Project ID or namespace/project_path"),
823
851
  noteable_type: z
@@ -1124,18 +1152,19 @@ export const GitLabWikiPageSchema = z.object({
1124
1152
  });
1125
1153
  // Merge Request Thread position schema - used for diff notes
1126
1154
  export const MergeRequestThreadPositionSchema = z.object({
1127
- base_sha: z.string().describe("Base commit SHA in the source branch"),
1128
- head_sha: z.string().describe("SHA referencing HEAD of the source branch"),
1129
- start_sha: z.string().describe("SHA referencing the start commit of the source branch"),
1130
- position_type: z.enum(["text", "image", "file"]).describe("Type of position reference"),
1131
- new_path: z.string().optional().describe("File path after change"),
1132
- old_path: z.string().optional().describe("File path before change"),
1133
- new_line: z.number().nullable().optional().describe("Line number after change"),
1134
- old_line: z.number().nullable().optional().describe("Line number before change"),
1135
- width: z.number().optional().describe("Width of the image (for image diffs)"),
1136
- height: z.number().optional().describe("Height of the image (for image diffs)"),
1137
- x: z.number().optional().describe("X coordinate on the image (for image diffs)"),
1138
- y: z.number().optional().describe("Y coordinate on the image (for image diffs)"),
1155
+ base_sha: z.string().describe("REQUIRED: Base commit SHA in the source branch. Get this from merge request diff_refs.base_sha."),
1156
+ head_sha: z.string().describe("REQUIRED: SHA referencing HEAD of the source branch. Get this from merge request diff_refs.head_sha."),
1157
+ start_sha: z.string().describe("REQUIRED: SHA referencing the start commit of the source branch. Get this from merge request diff_refs.start_sha."),
1158
+ position_type: z.enum(["text", "image", "file"]).describe("REQUIRED: Position type. Use 'text' for code diffs, 'image' for image diffs, 'file' for file-level comments."),
1159
+ new_path: z.string().optional().describe("File path after changes. REQUIRED for most diff comments. Use same as old_path if file wasn't renamed."),
1160
+ old_path: z.string().optional().describe("File path before changes. REQUIRED for most diff comments. Use same as new_path if file wasn't renamed."),
1161
+ new_line: z.number().nullable().optional().describe("Line number in modified file (after changes). Use for added lines or context lines. NULL for deleted lines. For single-line comments on new lines."),
1162
+ old_line: z.number().nullable().optional().describe("Line number in original file (before changes). Use for deleted lines or context lines. NULL for added lines. For single-line comments on old lines."),
1163
+ line_range: LineRangeSchema.optional().describe("MULTILINE COMMENTS: Specify start/end line positions for commenting on multiple lines. Alternative to single old_line/new_line."),
1164
+ width: z.number().optional().describe("IMAGE DIFFS ONLY: Width of the image (for position_type='image')."),
1165
+ height: z.number().optional().describe("IMAGE DIFFS ONLY: Height of the image (for position_type='image')."),
1166
+ x: z.number().optional().describe("IMAGE DIFFS ONLY: X coordinate on the image (for position_type='image')."),
1167
+ y: z.number().optional().describe("IMAGE DIFFS ONLY: Y coordinate on the image (for position_type='image')."),
1139
1168
  });
1140
1169
  // Schema for creating a new merge request thread
1141
1170
  export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
@@ -1202,3 +1231,28 @@ export const GetMilestoneMergeRequestsSchema = GetProjectMilestoneSchema.merge(P
1202
1231
  export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema;
1203
1232
  // Schema for getting burndown chart events for a milestone
1204
1233
  export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
1234
+ // Add schemas for commit operations
1235
+ export const ListCommitsSchema = z.object({
1236
+ project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
1237
+ ref_name: z.string().optional().describe("The name of a repository branch, tag or revision range, or if not given the default branch"),
1238
+ since: z.string().optional().describe("Only commits after or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"),
1239
+ until: z.string().optional().describe("Only commits before or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"),
1240
+ path: z.string().optional().describe("The file path"),
1241
+ author: z.string().optional().describe("Search commits by commit author"),
1242
+ all: z.boolean().optional().describe("Retrieve every commit from the repository"),
1243
+ with_stats: z.boolean().optional().describe("Stats about each commit are added to the response"),
1244
+ first_parent: z.boolean().optional().describe("Follow only the first parent commit upon seeing a merge commit"),
1245
+ order: z.enum(["default", "topo"]).optional().describe("List commits in order"),
1246
+ trailers: z.boolean().optional().describe("Parse and include Git trailers for every commit"),
1247
+ page: z.number().optional().describe("Page number for pagination (default: 1)"),
1248
+ per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"),
1249
+ });
1250
+ export const GetCommitSchema = z.object({
1251
+ project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
1252
+ sha: z.string().describe("The commit hash or name of a repository branch or tag"),
1253
+ stats: z.boolean().optional().describe("Include commit stats"),
1254
+ });
1255
+ export const GetCommitDiffSchema = z.object({
1256
+ project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
1257
+ sha: z.string().describe("The commit hash or name of a repository branch or tag"),
1258
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "1.0.63",
3
+ "version": "1.0.65",
4
4
  "description": "MCP server for using the GitLab API",
5
5
  "license": "MIT",
6
6
  "author": "zereight",
@@ -33,11 +33,13 @@
33
33
  "@modelcontextprotocol/sdk": "1.8.0",
34
34
  "@types/node-fetch": "^2.6.12",
35
35
  "express": "^5.1.0",
36
+ "fetch-cookie": "^3.1.0",
36
37
  "form-data": "^4.0.0",
37
38
  "http-proxy-agent": "^7.0.2",
38
39
  "https-proxy-agent": "^7.0.6",
39
40
  "node-fetch": "^3.3.2",
40
41
  "socks-proxy-agent": "^8.0.5",
42
+ "tough-cookie": "^5.1.2",
41
43
  "zod-to-json-schema": "^3.23.5"
42
44
  },
43
45
  "devDependencies": {