@zereight/mcp-gitlab 1.0.13 → 1.0.15

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/build/index.js CHANGED
@@ -10,7 +10,9 @@ import { dirname } from "path";
10
10
  import fs from "fs";
11
11
  import path from "path";
12
12
  import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, GitLabMergeRequestDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, CreateNoteSchema, } from "./schemas.js";
13
- // Read version from package.json
13
+ /**
14
+ * Read version from package.json
15
+ */
14
16
  const __filename = fileURLToPath(import.meta.url);
15
17
  const __dirname = dirname(__filename);
16
18
  const packageJsonPath = path.resolve(__dirname, '../package.json');
@@ -33,7 +35,12 @@ const server = new Server({
33
35
  },
34
36
  });
35
37
  const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
36
- // Smart URL handling for GitLab API
38
+ /**
39
+ * Smart URL handling for GitLab API
40
+ *
41
+ * @param {string | undefined} url - Input GitLab API URL
42
+ * @returns {string} Normalized GitLab API URL with /api/v4 path
43
+ */
37
44
  function normalizeGitLabApiUrl(url) {
38
45
  if (!url) {
39
46
  return "https://gitlab.com/api/v4";
@@ -59,20 +66,36 @@ if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
59
66
  console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
60
67
  process.exit(1);
61
68
  }
62
- // GitLab API 공통 헤더
69
+ /**
70
+ * Common headers for GitLab API requests
71
+ * GitLab API 공통 헤더 (Common headers for GitLab API)
72
+ */
63
73
  const DEFAULT_HEADERS = {
64
74
  Accept: "application/json",
65
75
  "Content-Type": "application/json",
66
76
  Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
67
77
  };
68
- // API 에러 처리를 위한 유틸리티 함수
78
+ /**
79
+ * Utility function for handling GitLab API errors
80
+ * API 에러 처리를 위한 유틸리티 함수 (Utility function for handling API errors)
81
+ *
82
+ * @param {import("node-fetch").Response} response - The response from GitLab API
83
+ * @throws {Error} Throws an error with response details if the request failed
84
+ */
69
85
  async function handleGitLabError(response) {
70
86
  if (!response.ok) {
71
87
  const errorBody = await response.text();
72
88
  throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
73
89
  }
74
90
  }
75
- // 프로젝트 포크 생성
91
+ /**
92
+ * Create a fork of a GitLab project
93
+ * 프로젝트 포크 생성 (Create a project fork)
94
+ *
95
+ * @param {string} projectId - The ID or URL-encoded path of the project
96
+ * @param {string} [namespace] - The namespace to fork the project to
97
+ * @returns {Promise<GitLabFork>} The created fork
98
+ */
76
99
  async function forkProject(projectId, namespace) {
77
100
  // API 엔드포인트 URL 생성
78
101
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`);
@@ -91,7 +114,14 @@ async function forkProject(projectId, namespace) {
91
114
  const data = await response.json();
92
115
  return GitLabForkSchema.parse(data);
93
116
  }
94
- // 새로운 브랜치 생성
117
+ /**
118
+ * Create a new branch in a GitLab project
119
+ * 새로운 브랜치 생성 (Create a new branch)
120
+ *
121
+ * @param {string} projectId - The ID or URL-encoded path of the project
122
+ * @param {z.infer<typeof CreateBranchOptionsSchema>} options - Branch creation options
123
+ * @returns {Promise<GitLabReference>} The created branch reference
124
+ */
95
125
  async function createBranch(projectId, options) {
96
126
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches`);
97
127
  const response = await fetch(url.toString(), {
@@ -105,7 +135,13 @@ async function createBranch(projectId, options) {
105
135
  await handleGitLabError(response);
106
136
  return GitLabReferenceSchema.parse(await response.json());
107
137
  }
108
- // 프로젝트의 기본 브랜치 조회
138
+ /**
139
+ * Get the default branch for a GitLab project
140
+ * 프로젝트의 기본 브랜치 조회 (Get the default branch of a project)
141
+ *
142
+ * @param {string} projectId - The ID or URL-encoded path of the project
143
+ * @returns {Promise<string>} The name of the default branch
144
+ */
109
145
  async function getDefaultBranchRef(projectId) {
110
146
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`);
111
147
  const response = await fetch(url.toString(), {
@@ -115,7 +151,15 @@ async function getDefaultBranchRef(projectId) {
115
151
  const project = GitLabRepositorySchema.parse(await response.json());
116
152
  return project.default_branch ?? "main";
117
153
  }
118
- // 파일 내용 조회
154
+ /**
155
+ * Get the contents of a file from a GitLab project
156
+ * 파일 내용 조회 (Get file contents)
157
+ *
158
+ * @param {string} projectId - The ID or URL-encoded path of the project
159
+ * @param {string} filePath - The path of the file to get
160
+ * @param {string} [ref] - The name of the branch, tag or commit
161
+ * @returns {Promise<GitLabContent>} The file content
162
+ */
119
163
  async function getFileContents(projectId, filePath, ref) {
120
164
  const encodedPath = encodeURIComponent(filePath);
121
165
  // ref가 없는 경우 default branch를 가져옴
@@ -141,7 +185,14 @@ async function getFileContents(projectId, filePath, ref) {
141
185
  }
142
186
  return parsedData;
143
187
  }
144
- // 이슈 생성
188
+ /**
189
+ * Create a new issue in a GitLab project
190
+ * 이슈 생성 (Create an issue)
191
+ *
192
+ * @param {string} projectId - The ID or URL-encoded path of the project
193
+ * @param {z.infer<typeof CreateIssueOptionsSchema>} options - Issue creation options
194
+ * @returns {Promise<GitLabIssue>} The created issue
195
+ */
145
196
  async function createIssue(projectId, options) {
146
197
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`);
147
198
  const response = await fetch(url.toString(), {
@@ -164,6 +215,14 @@ async function createIssue(projectId, options) {
164
215
  const data = await response.json();
165
216
  return GitLabIssueSchema.parse(data);
166
217
  }
218
+ /**
219
+ * Create a new merge request in a GitLab project
220
+ * 병합 요청 생성
221
+ *
222
+ * @param {string} projectId - The ID or URL-encoded path of the project
223
+ * @param {z.infer<typeof CreateMergeRequestOptionsSchema>} options - Merge request creation options
224
+ * @returns {Promise<GitLabMergeRequest>} The created merge request
225
+ */
167
226
  async function createMergeRequest(projectId, options) {
168
227
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`);
169
228
  const response = await fetch(url.toString(), {
@@ -193,7 +252,19 @@ async function createMergeRequest(projectId, options) {
193
252
  const data = await response.json();
194
253
  return GitLabMergeRequestSchema.parse(data);
195
254
  }
196
- async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath) {
255
+ /**
256
+ * Create or update a file in a GitLab project
257
+ * 파일 생성 또는 업데이트
258
+ *
259
+ * @param {string} projectId - The ID or URL-encoded path of the project
260
+ * @param {string} filePath - The path of the file to create or update
261
+ * @param {string} content - The content of the file
262
+ * @param {string} commitMessage - The commit message
263
+ * @param {string} branch - The branch name
264
+ * @param {string} [previousPath] - The previous path of the file in case of rename
265
+ * @returns {Promise<GitLabCreateUpdateFileResponse>} The file update response
266
+ */
267
+ async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath, last_commit_id, commit_id) {
197
268
  const encodedPath = encodeURIComponent(filePath);
198
269
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
199
270
  const body = {
@@ -206,14 +277,38 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
206
277
  // Check if file exists
207
278
  let method = "POST";
208
279
  try {
209
- await getFileContents(projectId, filePath, branch);
280
+ // Get file contents to check existence and retrieve commit IDs
281
+ const fileData = await getFileContents(projectId, filePath, branch);
210
282
  method = "PUT";
283
+ // If fileData is not an array, it's a file content object with commit IDs
284
+ if (!Array.isArray(fileData)) {
285
+ // Use commit IDs from the file data if not provided in parameters
286
+ if (!commit_id && fileData.commit_id) {
287
+ body.commit_id = fileData.commit_id;
288
+ }
289
+ else if (commit_id) {
290
+ body.commit_id = commit_id;
291
+ }
292
+ if (!last_commit_id && fileData.last_commit_id) {
293
+ body.last_commit_id = fileData.last_commit_id;
294
+ }
295
+ else if (last_commit_id) {
296
+ body.last_commit_id = last_commit_id;
297
+ }
298
+ }
211
299
  }
212
300
  catch (error) {
213
301
  if (!(error instanceof Error && error.message.includes("File not found"))) {
214
302
  throw error;
215
303
  }
216
- // File doesn't exist, use POST
304
+ // File doesn't exist, use POST - no need for commit IDs for new files
305
+ // But still use any provided as parameters if they exist
306
+ if (commit_id) {
307
+ body.commit_id = commit_id;
308
+ }
309
+ if (last_commit_id) {
310
+ body.last_commit_id = last_commit_id;
311
+ }
217
312
  }
218
313
  const response = await fetch(url.toString(), {
219
314
  method,
@@ -231,6 +326,15 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
231
326
  const data = await response.json();
232
327
  return GitLabCreateUpdateFileResponseSchema.parse(data);
233
328
  }
329
+ /**
330
+ * Create a tree structure in a GitLab project repository
331
+ * 저장소에 트리 구조 생성
332
+ *
333
+ * @param {string} projectId - The ID or URL-encoded path of the project
334
+ * @param {FileOperation[]} files - Array of file operations
335
+ * @param {string} [ref] - The name of the branch, tag or commit
336
+ * @returns {Promise<GitLabTree>} The created tree
337
+ */
234
338
  async function createTree(projectId, files, ref) {
235
339
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree`);
236
340
  if (ref) {
@@ -262,6 +366,16 @@ async function createTree(projectId, files, ref) {
262
366
  const data = await response.json();
263
367
  return GitLabTreeSchema.parse(data);
264
368
  }
369
+ /**
370
+ * Create a commit in a GitLab project repository
371
+ * 저장소에 커밋 생성
372
+ *
373
+ * @param {string} projectId - The ID or URL-encoded path of the project
374
+ * @param {string} message - The commit message
375
+ * @param {string} branch - The branch name
376
+ * @param {FileOperation[]} actions - Array of file operations for the commit
377
+ * @returns {Promise<GitLabCommit>} The created commit
378
+ */
265
379
  async function createCommit(projectId, message, branch, actions) {
266
380
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`);
267
381
  const response = await fetch(url.toString(), {
@@ -293,6 +407,15 @@ async function createCommit(projectId, message, branch, actions) {
293
407
  const data = await response.json();
294
408
  return GitLabCommitSchema.parse(data);
295
409
  }
410
+ /**
411
+ * Search for GitLab projects
412
+ * 프로젝트 검색
413
+ *
414
+ * @param {string} query - The search query
415
+ * @param {number} [page=1] - The page number
416
+ * @param {number} [perPage=20] - Number of items per page
417
+ * @returns {Promise<GitLabSearchResponse>} The search results
418
+ */
296
419
  async function searchProjects(query, page = 1, perPage = 20) {
297
420
  const url = new URL(`${GITLAB_API_URL}/projects`);
298
421
  url.searchParams.append("search", query);
@@ -323,6 +446,13 @@ async function searchProjects(query, page = 1, perPage = 20) {
323
446
  items: projects,
324
447
  });
325
448
  }
449
+ /**
450
+ * Create a new GitLab repository
451
+ * 새 저장소 생성
452
+ *
453
+ * @param {z.infer<typeof CreateRepositoryOptionsSchema>} options - Repository creation options
454
+ * @returns {Promise<GitLabRepository>} The created repository
455
+ */
326
456
  async function createRepository(options) {
327
457
  const response = await fetch(`${GITLAB_API_URL}/projects`, {
328
458
  method: "POST",
@@ -347,7 +477,14 @@ async function createRepository(options) {
347
477
  const data = await response.json();
348
478
  return GitLabRepositorySchema.parse(data);
349
479
  }
350
- // MR 조회 함수
480
+ /**
481
+ * Get merge request details
482
+ * MR 조회 함수 (Function to retrieve merge request)
483
+ *
484
+ * @param {string} projectId - The ID or URL-encoded path of the project
485
+ * @param {number} mergeRequestIid - The internal ID of the merge request
486
+ * @returns {Promise<GitLabMergeRequest>} The merge request details
487
+ */
351
488
  async function getMergeRequest(projectId, mergeRequestIid) {
352
489
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
353
490
  const response = await fetch(url.toString(), {
@@ -356,7 +493,15 @@ async function getMergeRequest(projectId, mergeRequestIid) {
356
493
  await handleGitLabError(response);
357
494
  return GitLabMergeRequestSchema.parse(await response.json());
358
495
  }
359
- // MR 변경사항 조회 함수
496
+ /**
497
+ * Get merge request changes/diffs
498
+ * MR 변경사항 조회 함수 (Function to retrieve merge request changes)
499
+ *
500
+ * @param {string} projectId - The ID or URL-encoded path of the project
501
+ * @param {number} mergeRequestIid - The internal ID of the merge request
502
+ * @param {string} [view] - The view type for the diff (inline or parallel)
503
+ * @returns {Promise<GitLabMergeRequestDiff[]>} The merge request diffs
504
+ */
360
505
  async function getMergeRequestDiffs(projectId, mergeRequestIid, view) {
361
506
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/changes`);
362
507
  if (view) {
@@ -369,7 +514,15 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, view) {
369
514
  const data = (await response.json());
370
515
  return z.array(GitLabMergeRequestDiffSchema).parse(data.changes);
371
516
  }
372
- // MR 업데이트 함수
517
+ /**
518
+ * Update a merge request
519
+ * MR 업데이트 함수 (Function to update merge request)
520
+ *
521
+ * @param {string} projectId - The ID or URL-encoded path of the project
522
+ * @param {number} mergeRequestIid - The internal ID of the merge request
523
+ * @param {Object} options - The update options
524
+ * @returns {Promise<GitLabMergeRequest>} The updated merge request
525
+ */
373
526
  async function updateMergeRequest(projectId, mergeRequestIid, options) {
374
527
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
375
528
  const response = await fetch(url.toString(), {
@@ -380,7 +533,17 @@ async function updateMergeRequest(projectId, mergeRequestIid, options) {
380
533
  await handleGitLabError(response);
381
534
  return GitLabMergeRequestSchema.parse(await response.json());
382
535
  }
383
- // 📦 새로운 함수: createNote - 이슈 또는 병합 요청에 노트(댓글)를 추가하는 함수
536
+ /**
537
+ * Create a new note (comment) on an issue or merge request
538
+ * 📦 새로운 함수: createNote - 이슈 또는 병합 요청에 노트(댓글)를 추가하는 함수
539
+ * (New function: createNote - Function to add a note (comment) to an issue or merge request)
540
+ *
541
+ * @param {string} projectId - The ID or URL-encoded path of the project
542
+ * @param {"issue" | "merge_request"} noteableType - The type of the item to add a note to (issue or merge_request)
543
+ * @param {number} noteableIid - The internal ID of the issue or merge request
544
+ * @param {string} body - The content of the note
545
+ * @returns {Promise<any>} The created note
546
+ */
384
547
  async function createNote(projectId, noteableType, // 'issue' 또는 'merge_request' 타입 명시
385
548
  noteableIid, body) {
386
549
  // ⚙️ 응답 타입은 GitLab API 문서에 따라 조정 가능
@@ -520,7 +683,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
520
683
  }
521
684
  case "create_or_update_file": {
522
685
  const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
523
- const result = await createOrUpdateFile(args.project_id, args.file_path, args.content, args.commit_message, args.branch, args.previous_path);
686
+ const result = await createOrUpdateFile(args.project_id, args.file_path, args.content, args.commit_message, args.branch, args.previous_path, args.last_commit_id, args.commit_id);
524
687
  return {
525
688
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
526
689
  };
@@ -597,6 +760,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
597
760
  throw error;
598
761
  }
599
762
  });
763
+ /**
764
+ * Initialize and run the server
765
+ * 서버 초기화 및 실행
766
+ */
600
767
  async function runServer() {
601
768
  try {
602
769
  console.error("========================");
package/build/schemas.js CHANGED
@@ -39,7 +39,9 @@ export const GitLabFileContentSchema = z.object({
39
39
  content_sha256: z.string(), // Changed from sha to match GitLab API
40
40
  ref: z.string(), // Added as GitLab requires branch reference
41
41
  blob_id: z.string(), // Added to match GitLab API
42
+ commit_id: z.string(), // ID of the current file version
42
43
  last_commit_id: z.string(), // Added to match GitLab API
44
+ execute_filemode: z.boolean().optional(), // Added to match GitLab API
43
45
  });
44
46
  export const GitLabDirectoryContentSchema = z.object({
45
47
  name: z.string(),
@@ -122,7 +124,7 @@ export const CreateBranchOptionsSchema = z.object({
122
124
  export const GitLabCreateUpdateFileResponseSchema = z.object({
123
125
  file_path: z.string(),
124
126
  branch: z.string(),
125
- commit_id: z.string(), // Changed from sha to match GitLab API
127
+ commit_id: z.string().optional(), // Optional since it's not always returned by the API
126
128
  content: GitLabFileContentSchema.optional(),
127
129
  });
128
130
  export const GitLabSearchResponseSchema = z.object({
@@ -236,6 +238,14 @@ export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
236
238
  .string()
237
239
  .optional()
238
240
  .describe("Path of the file to move/rename"),
241
+ last_commit_id: z
242
+ .string()
243
+ .optional()
244
+ .describe("Last known file commit ID"),
245
+ commit_id: z
246
+ .string()
247
+ .optional()
248
+ .describe("Current file commit ID (for update operations)"),
239
249
  });
240
250
  export const SearchRepositoriesSchema = z.object({
241
251
  search: z.string().describe("Search query"), // Changed from query to match GitLab API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "MCP server for using the GitLab API",
5
5
  "license": "MIT",
6
6
  "author": "zereight",