@zereight/mcp-gitlab 1.0.35 → 1.0.37

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
@@ -55,31 +55,33 @@ When using with the Claude App, you need to set up your API key and URLs directl
55
55
  11. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)
56
56
  12. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
57
57
  13. `create_note` - Create a new note (comment) to an issue or merge request
58
- 14. `mr_discussions` - List discussion items for a merge request
59
- 15. `update_merge_request_note` - Modify an existing merge request thread note
60
- 16. `list_issues` - List issues in a GitLab project with filtering options
61
- 17. `get_issue` - Get details of a specific issue in a GitLab project
62
- 18. `update_issue` - Update an issue in a GitLab project
63
- 19. `delete_issue` - Delete an issue from a GitLab project
64
- 20. `list_issue_links` - List all issue links for a specific issue
65
- 21. `get_issue_link` - Get a specific issue link
66
- 22. `create_issue_link` - Create an issue link between two issues
67
- 23. `delete_issue_link` - Delete an issue link
68
- 24. `list_namespaces` - List all namespaces available to the current user
69
- 25. `get_namespace` - Get details of a namespace by ID or path
70
- 26. `verify_namespace` - Verify if a namespace path exists
71
- 27. `get_project` - Get details of a specific project
72
- 28. `list_projects` - List projects accessible by the current user
73
- 29. `list_labels` - List labels for a project
74
- 30. `get_label` - Get a single label from a project
75
- 31. `create_label` - Create a new label in a project
76
- 32. `update_label` - Update an existing label in a project
77
- 33. `delete_label` - Delete a label from a project
78
- 34. `list_group_projects` - List projects in a GitLab group with filtering options
79
- 35. `list_wiki_pages` - List wiki pages in a GitLab project
80
- 36. `get_wiki_page` - Get details of a specific wiki page
81
- 37. `create_wiki_page` - Create a new wiki page in a GitLab project
82
- 38. `update_wiki_page` - Update an existing wiki page in a GitLab project
83
- 39. `delete_wiki_page` - Delete a wiki page from a GitLab project
84
- 40. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
58
+ 14. `create_merge_request_thread` - Create a new thread on a merge request
59
+ 15. `mr_discussions` - List discussion items for a merge request
60
+ 16. `update_merge_request_note` - Modify an existing merge request thread note
61
+ 17. `create_merge_request_note` - Add a new note to an existing merge request thread
62
+ 18. `list_issues` - List issues in a GitLab project with filtering options
63
+ 19. `get_issue` - Get details of a specific issue in a GitLab project
64
+ 20. `update_issue` - Update an issue in a GitLab project
65
+ 21. `delete_issue` - Delete an issue from a GitLab project
66
+ 22. `list_issue_links` - List all issue links for a specific issue
67
+ 23. `get_issue_link` - Get a specific issue link
68
+ 24. `create_issue_link` - Create an issue link between two issues
69
+ 25. `delete_issue_link` - Delete an issue link
70
+ 26. `list_namespaces` - List all namespaces available to the current user
71
+ 27. `get_namespace` - Get details of a namespace by ID or path
72
+ 28. `verify_namespace` - Verify if a namespace path exists
73
+ 29. `get_project` - Get details of a specific project
74
+ 30. `list_projects` - List projects accessible by the current user
75
+ 31. `list_labels` - List labels for a project
76
+ 32. `get_label` - Get a single label from a project
77
+ 33. `create_label` - Create a new label in a project
78
+ 34. `update_label` - Update an existing label in a project
79
+ 35. `delete_label` - Delete a label from a project
80
+ 36. `list_group_projects` - List projects in a GitLab group with filtering options
81
+ 37. `list_wiki_pages` - List wiki pages in a GitLab project
82
+ 38. `get_wiki_page` - Get details of a specific wiki page
83
+ 39. `create_wiki_page` - Create a new wiki page in a GitLab project
84
+ 40. `update_wiki_page` - Update an existing wiki page in a GitLab project
85
+ 41. `delete_wiki_page` - Delete a wiki page from a GitLab project
86
+ 42. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
85
87
  <!-- TOOLS-END -->
package/build/index.js CHANGED
@@ -13,10 +13,11 @@ import { dirname } from "path";
13
13
  import fs from "fs";
14
14
  import path from "path";
15
15
  import { URL } from "url";
16
- import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, GitLabNamespaceSchema, GitLabNamespaceExistsResponseSchema, GitLabProjectSchema, CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, GitLabMergeRequestDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, ListIssuesSchema, GetIssueSchema, UpdateIssueSchema, DeleteIssueSchema, GitLabIssueLinkSchema, GitLabIssueWithLinkDetailsSchema, ListIssueLinksSchema, GetIssueLinkSchema, CreateIssueLinkSchema, DeleteIssueLinkSchema, ListNamespacesSchema, GetNamespaceSchema, VerifyNamespaceSchema, GetProjectSchema, ListProjectsSchema, ListLabelsSchema, GetLabelSchema, CreateLabelSchema, UpdateLabelSchema, DeleteLabelSchema, CreateNoteSchema, ListGroupProjectsSchema, ListWikiPagesSchema, GetWikiPageSchema, CreateWikiPageSchema, UpdateWikiPageSchema, DeleteWikiPageSchema, GitLabWikiPageSchema,
16
+ import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, GitLabNamespaceSchema, GitLabNamespaceExistsResponseSchema, GitLabProjectSchema, CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, GitLabMergeRequestDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, ListIssuesSchema, GetIssueSchema, UpdateIssueSchema, DeleteIssueSchema, GitLabIssueLinkSchema, GitLabIssueWithLinkDetailsSchema, ListIssueLinksSchema, GetIssueLinkSchema, CreateIssueLinkSchema, DeleteIssueLinkSchema, ListNamespacesSchema, GetNamespaceSchema, VerifyNamespaceSchema, GetProjectSchema, ListProjectsSchema, ListLabelsSchema, GetLabelSchema, CreateLabelSchema, UpdateLabelSchema, DeleteLabelSchema, CreateNoteSchema, CreateMergeRequestThreadSchema, ListGroupProjectsSchema, ListWikiPagesSchema, GetWikiPageSchema, CreateWikiPageSchema, UpdateWikiPageSchema, DeleteWikiPageSchema, GitLabWikiPageSchema,
17
17
  // Discussion Schemas
18
18
  GitLabDiscussionNoteSchema, // Added
19
19
  GitLabDiscussionSchema, UpdateMergeRequestNoteSchema, // Added
20
+ CreateMergeRequestNoteSchema, // Added
20
21
  ListMergeRequestDiscussionsSchema, GitLabTreeItemSchema, GetRepositoryTreeSchema, } from "./schemas.js";
21
22
  /**
22
23
  * Read version from package.json
@@ -150,6 +151,11 @@ const allTools = [
150
151
  description: "Create a new note (comment) to an issue or merge request",
151
152
  inputSchema: zodToJsonSchema(CreateNoteSchema),
152
153
  },
154
+ {
155
+ name: "create_merge_request_thread",
156
+ description: "Create a new thread on a merge request",
157
+ inputSchema: zodToJsonSchema(CreateMergeRequestThreadSchema),
158
+ },
153
159
  {
154
160
  name: "mr_discussions",
155
161
  description: "List discussion items for a merge request",
@@ -160,6 +166,11 @@ const allTools = [
160
166
  description: "Modify an existing merge request thread note",
161
167
  inputSchema: zodToJsonSchema(UpdateMergeRequestNoteSchema),
162
168
  },
169
+ {
170
+ name: "create_merge_request_note",
171
+ description: "Add a new note to an existing merge request thread",
172
+ inputSchema: zodToJsonSchema(CreateMergeRequestNoteSchema),
173
+ },
163
174
  {
164
175
  name: "list_issues",
165
176
  description: "List issues in a GitLab project with filtering options",
@@ -373,6 +384,7 @@ async function handleGitLabError(response) {
373
384
  * @returns {Promise<GitLabFork>} The created fork
374
385
  */
375
386
  async function forkProject(projectId, namespace) {
387
+ projectId = decodeURIComponent(projectId); // Decode project ID
376
388
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`);
377
389
  if (namespace) {
378
390
  url.searchParams.append("namespace", namespace);
@@ -398,6 +410,7 @@ async function forkProject(projectId, namespace) {
398
410
  * @returns {Promise<GitLabReference>} The created branch reference
399
411
  */
400
412
  async function createBranch(projectId, options) {
413
+ projectId = decodeURIComponent(projectId); // Decode project ID
401
414
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches`);
402
415
  const response = await fetch(url.toString(), {
403
416
  ...DEFAULT_FETCH_CONFIG,
@@ -418,6 +431,7 @@ async function createBranch(projectId, options) {
418
431
  * @returns {Promise<string>} The name of the default branch
419
432
  */
420
433
  async function getDefaultBranchRef(projectId) {
434
+ projectId = decodeURIComponent(projectId); // Decode project ID
421
435
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`);
422
436
  const response = await fetch(url.toString(), {
423
437
  ...DEFAULT_FETCH_CONFIG,
@@ -436,6 +450,7 @@ async function getDefaultBranchRef(projectId) {
436
450
  * @returns {Promise<GitLabContent>} The file content
437
451
  */
438
452
  async function getFileContents(projectId, filePath, ref) {
453
+ projectId = decodeURIComponent(projectId); // Decode project ID
439
454
  const encodedPath = encodeURIComponent(filePath);
440
455
  // ref가 없는 경우 default branch를 가져옴
441
456
  if (!ref) {
@@ -469,6 +484,7 @@ async function getFileContents(projectId, filePath, ref) {
469
484
  * @returns {Promise<GitLabIssue>} The created issue
470
485
  */
471
486
  async function createIssue(projectId, options) {
487
+ projectId = decodeURIComponent(projectId); // Decode project ID
472
488
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`);
473
489
  const response = await fetch(url.toString(), {
474
490
  ...DEFAULT_FETCH_CONFIG,
@@ -499,6 +515,7 @@ async function createIssue(projectId, options) {
499
515
  * @returns {Promise<GitLabIssue[]>} List of issues
500
516
  */
501
517
  async function listIssues(projectId, options = {}) {
518
+ projectId = decodeURIComponent(projectId); // Decode project ID
502
519
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`);
503
520
  // Add all query parameters
504
521
  Object.entries(options).forEach(([key, value]) => {
@@ -528,6 +545,7 @@ async function listIssues(projectId, options = {}) {
528
545
  * @returns {Promise<GitLabIssue>} The issue
529
546
  */
530
547
  async function getIssue(projectId, issueIid) {
548
+ projectId = decodeURIComponent(projectId); // Decode project ID
531
549
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`);
532
550
  const response = await fetch(url.toString(), {
533
551
  ...DEFAULT_FETCH_CONFIG,
@@ -546,6 +564,7 @@ async function getIssue(projectId, issueIid) {
546
564
  * @returns {Promise<GitLabIssue>} The updated issue
547
565
  */
548
566
  async function updateIssue(projectId, issueIid, options) {
567
+ projectId = decodeURIComponent(projectId); // Decode project ID
549
568
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`);
550
569
  // Convert labels array to comma-separated string if present
551
570
  const body = { ...options };
@@ -570,6 +589,7 @@ async function updateIssue(projectId, issueIid, options) {
570
589
  * @returns {Promise<void>}
571
590
  */
572
591
  async function deleteIssue(projectId, issueIid) {
592
+ projectId = decodeURIComponent(projectId); // Decode project ID
573
593
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`);
574
594
  const response = await fetch(url.toString(), {
575
595
  ...DEFAULT_FETCH_CONFIG,
@@ -586,6 +606,7 @@ async function deleteIssue(projectId, issueIid) {
586
606
  * @returns {Promise<GitLabIssueWithLinkDetails[]>} List of issues with link details
587
607
  */
588
608
  async function listIssueLinks(projectId, issueIid) {
609
+ projectId = decodeURIComponent(projectId); // Decode project ID
589
610
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links`);
590
611
  const response = await fetch(url.toString(), {
591
612
  ...DEFAULT_FETCH_CONFIG,
@@ -604,6 +625,7 @@ async function listIssueLinks(projectId, issueIid) {
604
625
  * @returns {Promise<GitLabIssueLink>} The issue link
605
626
  */
606
627
  async function getIssueLink(projectId, issueIid, issueLinkId) {
628
+ projectId = decodeURIComponent(projectId); // Decode project ID
607
629
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}`);
608
630
  const response = await fetch(url.toString(), {
609
631
  ...DEFAULT_FETCH_CONFIG,
@@ -624,6 +646,8 @@ async function getIssueLink(projectId, issueIid, issueLinkId) {
624
646
  * @returns {Promise<GitLabIssueLink>} The created issue link
625
647
  */
626
648
  async function createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = "relates_to") {
649
+ projectId = decodeURIComponent(projectId); // Decode project ID
650
+ targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well
627
651
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links`);
628
652
  const response = await fetch(url.toString(), {
629
653
  ...DEFAULT_FETCH_CONFIG,
@@ -648,6 +672,7 @@ async function createIssueLink(projectId, issueIid, targetProjectId, targetIssue
648
672
  * @returns {Promise<void>}
649
673
  */
650
674
  async function deleteIssueLink(projectId, issueIid, issueLinkId) {
675
+ projectId = decodeURIComponent(projectId); // Decode project ID
651
676
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}`);
652
677
  const response = await fetch(url.toString(), {
653
678
  ...DEFAULT_FETCH_CONFIG,
@@ -664,6 +689,7 @@ async function deleteIssueLink(projectId, issueIid, issueLinkId) {
664
689
  * @returns {Promise<GitLabMergeRequest>} The created merge request
665
690
  */
666
691
  async function createMergeRequest(projectId, options) {
692
+ projectId = decodeURIComponent(projectId); // Decode project ID
667
693
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`);
668
694
  const response = await fetch(url.toString(), {
669
695
  ...DEFAULT_FETCH_CONFIG,
@@ -697,6 +723,7 @@ async function createMergeRequest(projectId, options) {
697
723
  * @returns {Promise<GitLabDiscussion[]>} List of discussions
698
724
  */
699
725
  async function listMergeRequestDiscussions(projectId, mergeRequestIid) {
726
+ projectId = decodeURIComponent(projectId); // Decode project ID
700
727
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions`);
701
728
  const response = await fetch(url.toString(), {
702
729
  ...DEFAULT_FETCH_CONFIG,
@@ -719,9 +746,14 @@ async function listMergeRequestDiscussions(projectId, mergeRequestIid) {
719
746
  * @returns {Promise<GitLabDiscussionNote>} The updated note
720
747
  */
721
748
  async function updateMergeRequestNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
749
+ projectId = decodeURIComponent(projectId); // Decode project ID
722
750
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
723
- const payload = { body };
724
- if (resolved !== undefined) {
751
+ // Only one of body or resolved can be sent according to GitLab API
752
+ const payload = {};
753
+ if (body !== undefined) {
754
+ payload.body = body;
755
+ }
756
+ else if (resolved !== undefined) {
725
757
  payload.resolved = resolved;
726
758
  }
727
759
  const response = await fetch(url.toString(), {
@@ -733,6 +765,33 @@ async function updateMergeRequestNote(projectId, mergeRequestIid, discussionId,
733
765
  const data = await response.json();
734
766
  return GitLabDiscussionNoteSchema.parse(data);
735
767
  }
768
+ /**
769
+ * Add a new note to an existing merge request thread
770
+ * 기존 병합 요청 스레드에 새 노트 추가
771
+ *
772
+ * @param {string} projectId - The ID or URL-encoded path of the project
773
+ * @param {number} mergeRequestIid - The IID of a merge request
774
+ * @param {string} discussionId - The ID of a thread
775
+ * @param {string} body - The content of the new note
776
+ * @param {string} [createdAt] - The creation date of the note (ISO 8601 format)
777
+ * @returns {Promise<GitLabDiscussionNote>} The created note
778
+ */
779
+ async function createMergeRequestNote(projectId, mergeRequestIid, discussionId, body, createdAt) {
780
+ projectId = decodeURIComponent(projectId); // Decode project ID
781
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes`);
782
+ const payload = { body };
783
+ if (createdAt) {
784
+ payload.created_at = createdAt;
785
+ }
786
+ const response = await fetch(url.toString(), {
787
+ ...DEFAULT_FETCH_CONFIG,
788
+ method: "POST",
789
+ body: JSON.stringify(payload),
790
+ });
791
+ await handleGitLabError(response);
792
+ const data = await response.json();
793
+ return GitLabDiscussionNoteSchema.parse(data);
794
+ }
736
795
  /**
737
796
  * Create or update a file in a GitLab project
738
797
  * 파일 생성 또는 업데이트
@@ -746,6 +805,7 @@ async function updateMergeRequestNote(projectId, mergeRequestIid, discussionId,
746
805
  * @returns {Promise<GitLabCreateUpdateFileResponse>} The file update response
747
806
  */
748
807
  async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath, last_commit_id, commit_id) {
808
+ projectId = decodeURIComponent(projectId); // Decode project ID
749
809
  const encodedPath = encodeURIComponent(filePath);
750
810
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
751
811
  const body = {
@@ -813,6 +873,7 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
813
873
  * @returns {Promise<GitLabTree>} The created tree
814
874
  */
815
875
  async function createTree(projectId, files, ref) {
876
+ projectId = decodeURIComponent(projectId); // Decode project ID
816
877
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree`);
817
878
  if (ref) {
818
879
  url.searchParams.append("ref", ref);
@@ -850,6 +911,7 @@ async function createTree(projectId, files, ref) {
850
911
  * @returns {Promise<GitLabCommit>} The created commit
851
912
  */
852
913
  async function createCommit(projectId, message, branch, actions) {
914
+ projectId = decodeURIComponent(projectId); // Decode project ID
853
915
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`);
854
916
  const response = await fetch(url.toString(), {
855
917
  ...DEFAULT_FETCH_CONFIG,
@@ -948,6 +1010,7 @@ async function createRepository(options) {
948
1010
  * @returns {Promise<GitLabMergeRequest>} The merge request details
949
1011
  */
950
1012
  async function getMergeRequest(projectId, mergeRequestIid, branchName) {
1013
+ projectId = decodeURIComponent(projectId); // Decode project ID
951
1014
  let url;
952
1015
  if (mergeRequestIid) {
953
1016
  url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
@@ -980,6 +1043,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
980
1043
  * @returns {Promise<GitLabMergeRequestDiff[]>} The merge request diffs
981
1044
  */
982
1045
  async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view) {
1046
+ projectId = decodeURIComponent(projectId); // Decode project ID
983
1047
  if (!mergeRequestIid && !branchName) {
984
1048
  throw new Error("Either mergeRequestIid or branchName must be provided");
985
1049
  }
@@ -1009,6 +1073,7 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view
1009
1073
  * @returns {Promise<GitLabMergeRequest>} The updated merge request
1010
1074
  */
1011
1075
  async function updateMergeRequest(projectId, options, mergeRequestIid, branchName) {
1076
+ projectId = decodeURIComponent(projectId); // Decode project ID
1012
1077
  if (!mergeRequestIid && !branchName) {
1013
1078
  throw new Error("Either mergeRequestIid or branchName must be provided");
1014
1079
  }
@@ -1038,6 +1103,7 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
1038
1103
  */
1039
1104
  async function createNote(projectId, noteableType, // 'issue' 또는 'merge_request' 타입 명시
1040
1105
  noteableIid, body) {
1106
+ projectId = decodeURIComponent(projectId); // Decode project ID
1041
1107
  // ⚙️ 응답 타입은 GitLab API 문서에 따라 조정 가능
1042
1108
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/${noteableType}s/${noteableIid}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation
1043
1109
  );
@@ -1052,6 +1118,43 @@ noteableIid, body) {
1052
1118
  }
1053
1119
  return await response.json();
1054
1120
  }
1121
+ /**
1122
+ * Create a new thread on a merge request
1123
+ * 📦 새로운 함수: createMergeRequestThread - 병합 요청에 새로운 스레드(토론)를 생성하는 함수
1124
+ * (New function: createMergeRequestThread - Function to create a new thread (discussion) on a merge request)
1125
+ *
1126
+ * This function provides more capabilities than createNote, including the ability to:
1127
+ * - Create diff notes (comments on specific lines of code)
1128
+ * - Specify exact positions for comments
1129
+ * - Set creation timestamps
1130
+ *
1131
+ * @param {string} projectId - The ID or URL-encoded path of the project
1132
+ * @param {number} mergeRequestIid - The internal ID of the merge request
1133
+ * @param {string} body - The content of the thread
1134
+ * @param {MergeRequestThreadPosition} [position] - Position information for diff notes
1135
+ * @param {string} [createdAt] - ISO 8601 formatted creation date
1136
+ * @returns {Promise<GitLabDiscussion>} The created discussion thread
1137
+ */
1138
+ async function createMergeRequestThread(projectId, mergeRequestIid, body, position, createdAt) {
1139
+ projectId = decodeURIComponent(projectId); // Decode project ID
1140
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions`);
1141
+ const payload = { body };
1142
+ // Add optional parameters if provided
1143
+ if (position) {
1144
+ payload.position = position;
1145
+ }
1146
+ if (createdAt) {
1147
+ payload.created_at = createdAt;
1148
+ }
1149
+ const response = await fetch(url.toString(), {
1150
+ ...DEFAULT_FETCH_CONFIG,
1151
+ method: "POST",
1152
+ body: JSON.stringify(payload),
1153
+ });
1154
+ await handleGitLabError(response);
1155
+ const data = await response.json();
1156
+ return GitLabDiscussionSchema.parse(data);
1157
+ }
1055
1158
  /**
1056
1159
  * List all namespaces
1057
1160
  * 사용 가능한 모든 네임스페이스 목록 조회
@@ -1128,6 +1231,7 @@ async function verifyNamespaceExistence(namespacePath, parentId) {
1128
1231
  * @returns {Promise<GitLabProject>} Project details
1129
1232
  */
1130
1233
  async function getProject(projectId, options = {}) {
1234
+ projectId = decodeURIComponent(projectId); // Decode project ID
1131
1235
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`);
1132
1236
  if (options.license) {
1133
1237
  url.searchParams.append("license", "true");
@@ -1183,6 +1287,7 @@ async function listProjects(options = {}) {
1183
1287
  * @returns Array of GitLab labels
1184
1288
  */
1185
1289
  async function listLabels(projectId, options = {}) {
1290
+ projectId = decodeURIComponent(projectId); // Decode project ID
1186
1291
  // Construct the URL with project path
1187
1292
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`);
1188
1293
  // Add query parameters
@@ -1215,6 +1320,7 @@ async function listLabels(projectId, options = {}) {
1215
1320
  * @returns GitLab label
1216
1321
  */
1217
1322
  async function getLabel(projectId, labelId, includeAncestorGroups) {
1323
+ projectId = decodeURIComponent(projectId); // Decode project ID
1218
1324
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`);
1219
1325
  // Add query parameters
1220
1326
  if (includeAncestorGroups !== undefined) {
@@ -1238,6 +1344,7 @@ async function getLabel(projectId, labelId, includeAncestorGroups) {
1238
1344
  * @returns Created GitLab label
1239
1345
  */
1240
1346
  async function createLabel(projectId, options) {
1347
+ projectId = decodeURIComponent(projectId); // Decode project ID
1241
1348
  // Make the API request
1242
1349
  const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`, {
1243
1350
  ...DEFAULT_FETCH_CONFIG,
@@ -1259,6 +1366,7 @@ async function createLabel(projectId, options) {
1259
1366
  * @returns Updated GitLab label
1260
1367
  */
1261
1368
  async function updateLabel(projectId, labelId, options) {
1369
+ projectId = decodeURIComponent(projectId); // Decode project ID
1262
1370
  // Make the API request
1263
1371
  const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, {
1264
1372
  ...DEFAULT_FETCH_CONFIG,
@@ -1278,6 +1386,7 @@ async function updateLabel(projectId, labelId, options) {
1278
1386
  * @param labelId The ID or name of the label to delete
1279
1387
  */
1280
1388
  async function deleteLabel(projectId, labelId) {
1389
+ projectId = decodeURIComponent(projectId); // Decode project ID
1281
1390
  // Make the API request
1282
1391
  const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, {
1283
1392
  ...DEFAULT_FETCH_CONFIG,
@@ -1339,6 +1448,7 @@ async function listGroupProjects(options) {
1339
1448
  * List wiki pages in a project
1340
1449
  */
1341
1450
  async function listWikiPages(projectId, options = {}) {
1451
+ projectId = decodeURIComponent(projectId); // Decode project ID
1342
1452
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis`);
1343
1453
  if (options.page)
1344
1454
  url.searchParams.append("page", options.page.toString());
@@ -1355,6 +1465,7 @@ async function listWikiPages(projectId, options = {}) {
1355
1465
  * Get a specific wiki page
1356
1466
  */
1357
1467
  async function getWikiPage(projectId, slug) {
1468
+ projectId = decodeURIComponent(projectId); // Decode project ID
1358
1469
  const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(slug)}`, { ...DEFAULT_FETCH_CONFIG });
1359
1470
  await handleGitLabError(response);
1360
1471
  const data = await response.json();
@@ -1364,6 +1475,7 @@ async function getWikiPage(projectId, slug) {
1364
1475
  * Create a new wiki page
1365
1476
  */
1366
1477
  async function createWikiPage(projectId, title, content, format) {
1478
+ projectId = decodeURIComponent(projectId); // Decode project ID
1367
1479
  const body = { title, content };
1368
1480
  if (format)
1369
1481
  body.format = format;
@@ -1380,6 +1492,7 @@ async function createWikiPage(projectId, title, content, format) {
1380
1492
  * Update an existing wiki page
1381
1493
  */
1382
1494
  async function updateWikiPage(projectId, slug, title, content, format) {
1495
+ projectId = decodeURIComponent(projectId); // Decode project ID
1383
1496
  const body = {};
1384
1497
  if (title)
1385
1498
  body.title = title;
@@ -1400,6 +1513,7 @@ async function updateWikiPage(projectId, slug, title, content, format) {
1400
1513
  * Delete a wiki page
1401
1514
  */
1402
1515
  async function deleteWikiPage(projectId, slug) {
1516
+ projectId = decodeURIComponent(projectId); // Decode project ID
1403
1517
  const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(slug)}`, {
1404
1518
  ...DEFAULT_FETCH_CONFIG,
1405
1519
  method: "DELETE",
@@ -1413,6 +1527,7 @@ async function deleteWikiPage(projectId, slug) {
1413
1527
  * @returns {Promise<GitLabTreeItem[]>}
1414
1528
  */
1415
1529
  async function getRepositoryTree(options) {
1530
+ options.project_id = decodeURIComponent(options.project_id); // Decode project_id within options
1416
1531
  const queryParams = new URLSearchParams();
1417
1532
  if (options.path)
1418
1533
  queryParams.append("path", options.path);
@@ -1447,11 +1562,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1447
1562
  ? allTools.filter((tool) => readOnlyTools.includes(tool.name))
1448
1563
  : allTools;
1449
1564
  // Toggle wiki tools by USE_GITLAB_WIKI flag
1450
- const tools = USE_GITLAB_WIKI
1565
+ let tools = USE_GITLAB_WIKI
1451
1566
  ? tools0
1452
1567
  : tools0.filter((tool) => !wikiToolNames.includes(tool.name));
1568
+ // <<< START: Gemini 호환성을 위해 $schema 제거 >>>
1569
+ tools = tools.map((tool) => {
1570
+ // inputSchema가 존재하고 객체인지 확인
1571
+ if (tool.inputSchema &&
1572
+ typeof tool.inputSchema === "object" &&
1573
+ tool.inputSchema !== null) {
1574
+ // $schema 키가 존재하면 삭제
1575
+ if ("$schema" in tool.inputSchema) {
1576
+ // 불변성을 위해 새로운 객체 생성 (선택적이지만 권장)
1577
+ const modifiedSchema = { ...tool.inputSchema };
1578
+ delete modifiedSchema.$schema;
1579
+ return { ...tool, inputSchema: modifiedSchema };
1580
+ }
1581
+ }
1582
+ // 변경이 필요 없으면 그대로 반환
1583
+ return tool;
1584
+ });
1585
+ // <<< END: Gemini 호환성을 위해 $schema 제거 >>>
1453
1586
  return {
1454
- tools,
1587
+ tools, // $schema가 제거된 도구 목록 반환
1455
1588
  };
1456
1589
  });
1457
1590
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -1557,12 +1690,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1557
1690
  }
1558
1691
  case "update_merge_request_note": {
1559
1692
  const args = UpdateMergeRequestNoteSchema.parse(request.params.arguments);
1560
- const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.discussion_id, args.note_id, args.body, args.resolved // Pass resolved if provided
1693
+ const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.discussion_id, args.note_id, args.body, // Now optional
1694
+ args.resolved // Now one of body or resolved must be provided, not both
1561
1695
  );
1562
1696
  return {
1563
1697
  content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
1564
1698
  };
1565
1699
  }
1700
+ case "create_merge_request_note": {
1701
+ const args = CreateMergeRequestNoteSchema.parse(request.params.arguments);
1702
+ const note = await createMergeRequestNote(args.project_id, args.merge_request_iid, args.discussion_id, args.body, args.created_at);
1703
+ return {
1704
+ content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
1705
+ };
1706
+ }
1566
1707
  case "get_merge_request": {
1567
1708
  const args = GetMergeRequestSchema.parse(request.params.arguments);
1568
1709
  const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid, args.source_branch);
@@ -1681,6 +1822,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1681
1822
  content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
1682
1823
  };
1683
1824
  }
1825
+ case "create_merge_request_thread": {
1826
+ const args = CreateMergeRequestThreadSchema.parse(request.params.arguments);
1827
+ const { project_id, merge_request_iid, body, position, created_at } = args;
1828
+ const thread = await createMergeRequestThread(project_id, merge_request_iid, body, position, created_at);
1829
+ return {
1830
+ content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
1831
+ };
1832
+ }
1684
1833
  case "list_issues": {
1685
1834
  const args = ListIssuesSchema.parse(request.params.arguments);
1686
1835
  const { project_id, ...options } = args;
package/build/schemas.js CHANGED
@@ -434,8 +434,19 @@ export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({
434
434
  merge_request_iid: z.number().describe("The IID of a merge request"),
435
435
  discussion_id: z.string().describe("The ID of a thread"),
436
436
  note_id: z.number().describe("The ID of a thread note"),
437
+ body: z.string().optional().describe("The content of the note or reply"),
438
+ resolved: z.boolean().optional().describe("Resolve or unresolve the note"),
439
+ }).refine(data => data.body !== undefined || data.resolved !== undefined, {
440
+ message: "At least one of 'body' or 'resolved' must be provided"
441
+ }).refine(data => !(data.body !== undefined && data.resolved !== undefined), {
442
+ message: "Only one of 'body' or 'resolved' can be provided, not both"
443
+ });
444
+ // Input schema for adding a note to an existing merge request discussion
445
+ export const CreateMergeRequestNoteSchema = ProjectParamsSchema.extend({
446
+ merge_request_iid: z.number().describe("The IID of a merge request"),
447
+ discussion_id: z.string().describe("The ID of a thread"),
437
448
  body: z.string().describe("The content of the note or reply"),
438
- resolved: z.boolean().optional().describe("Resolve or unresolve the note"), // Optional based on API docs
449
+ created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"),
439
450
  });
440
451
  // API Operation Parameter Schemas
441
452
  export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
@@ -919,3 +930,25 @@ export const GitLabWikiPageSchema = z.object({
919
930
  created_at: z.string().optional(),
920
931
  updated_at: z.string().optional(),
921
932
  });
933
+ // Merge Request Thread position schema - used for diff notes
934
+ export const MergeRequestThreadPositionSchema = z.object({
935
+ base_sha: z.string().describe("Base commit SHA in the source branch"),
936
+ head_sha: z.string().describe("SHA referencing HEAD of the source branch"),
937
+ start_sha: z.string().describe("SHA referencing the start commit of the source branch"),
938
+ position_type: z.enum(["text", "image", "file"]).describe("Type of position reference"),
939
+ new_path: z.string().optional().describe("File path after change"),
940
+ old_path: z.string().optional().describe("File path before change"),
941
+ new_line: z.number().nullable().optional().describe("Line number after change"),
942
+ old_line: z.number().nullable().optional().describe("Line number before change"),
943
+ width: z.number().optional().describe("Width of the image (for image diffs)"),
944
+ height: z.number().optional().describe("Height of the image (for image diffs)"),
945
+ x: z.number().optional().describe("X coordinate on the image (for image diffs)"),
946
+ y: z.number().optional().describe("Y coordinate on the image (for image diffs)"),
947
+ });
948
+ // Schema for creating a new merge request thread
949
+ export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
950
+ merge_request_iid: z.number().describe("The IID of a merge request"),
951
+ body: z.string().describe("The content of the thread"),
952
+ position: MergeRequestThreadPositionSchema.optional().describe("Position when creating a diff note"),
953
+ created_at: z.string().optional().describe("Date the thread was created at (ISO 8601 format)"),
954
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "MCP server for using the GitLab API",
5
5
  "license": "MIT",
6
6
  "author": "zereight",