@zereight/mcp-gitlab 1.0.11 → 1.0.13

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
@@ -5,17 +5,56 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
5
5
  import fetch from "node-fetch";
6
6
  import { z } from "zod";
7
7
  import { zodToJsonSchema } from "zod-to-json-schema";
8
+ import { fileURLToPath } from "url";
9
+ import { dirname } from "path";
10
+ import fs from "fs";
11
+ import path from "path";
8
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
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ const packageJsonPath = path.resolve(__dirname, '../package.json');
17
+ let SERVER_VERSION = "unknown";
18
+ try {
19
+ if (fs.existsSync(packageJsonPath)) {
20
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
21
+ SERVER_VERSION = packageJson.version || SERVER_VERSION;
22
+ }
23
+ }
24
+ catch (error) {
25
+ console.error("Warning: Could not read version from package.json:", error);
26
+ }
9
27
  const server = new Server({
10
28
  name: "better-gitlab-mcp-server",
11
- version: "0.0.1",
29
+ version: SERVER_VERSION,
12
30
  }, {
13
31
  capabilities: {
14
32
  tools: {},
15
33
  },
16
34
  });
17
35
  const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
18
- const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com/api/v4";
36
+ // Smart URL handling for GitLab API
37
+ function normalizeGitLabApiUrl(url) {
38
+ if (!url) {
39
+ return "https://gitlab.com/api/v4";
40
+ }
41
+ // Remove trailing slash if present
42
+ let normalizedUrl = url.endsWith('/') ? url.slice(0, -1) : url;
43
+ // Check if URL already has /api/v4
44
+ if (!normalizedUrl.endsWith('/api/v4') && !normalizedUrl.endsWith('/api/v4/')) {
45
+ // Append /api/v4 if not already present
46
+ normalizedUrl = `${normalizedUrl}/api/v4`;
47
+ }
48
+ return normalizedUrl;
49
+ }
50
+ // Use the normalizeGitLabApiUrl function to handle various URL formats
51
+ const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || "");
52
+ // Add debug logging for API URL construction
53
+ console.log("=== MCP Server Configuration ===");
54
+ console.log(`GITLAB_API_URL = "${GITLAB_API_URL}"`);
55
+ console.log(`Example project API URL = "${GITLAB_API_URL}/projects/123"`);
56
+ console.log(`Example Notes API URL = "${GITLAB_API_URL}/projects/123/issues/1/notes"`);
57
+ console.log("===============================");
19
58
  if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
20
59
  console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
21
60
  process.exit(1);
@@ -36,7 +75,7 @@ async function handleGitLabError(response) {
36
75
  // 프로젝트 포크 생성
37
76
  async function forkProject(projectId, namespace) {
38
77
  // API 엔드포인트 URL 생성
39
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/fork`);
78
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`);
40
79
  if (namespace) {
41
80
  url.searchParams.append("namespace", namespace);
42
81
  }
@@ -54,7 +93,7 @@ async function forkProject(projectId, namespace) {
54
93
  }
55
94
  // 새로운 브랜치 생성
56
95
  async function createBranch(projectId, options) {
57
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/branches`);
96
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches`);
58
97
  const response = await fetch(url.toString(), {
59
98
  method: "POST",
60
99
  headers: DEFAULT_HEADERS,
@@ -68,7 +107,7 @@ async function createBranch(projectId, options) {
68
107
  }
69
108
  // 프로젝트의 기본 브랜치 조회
70
109
  async function getDefaultBranchRef(projectId) {
71
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}`);
110
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`);
72
111
  const response = await fetch(url.toString(), {
73
112
  headers: DEFAULT_HEADERS,
74
113
  });
@@ -83,7 +122,7 @@ async function getFileContents(projectId, filePath, ref) {
83
122
  if (!ref) {
84
123
  ref = await getDefaultBranchRef(projectId);
85
124
  }
86
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
125
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
87
126
  url.searchParams.append("ref", ref);
88
127
  const response = await fetch(url.toString(), {
89
128
  headers: DEFAULT_HEADERS,
@@ -104,7 +143,7 @@ async function getFileContents(projectId, filePath, ref) {
104
143
  }
105
144
  // 이슈 생성
106
145
  async function createIssue(projectId, options) {
107
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/issues`);
146
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`);
108
147
  const response = await fetch(url.toString(), {
109
148
  method: "POST",
110
149
  headers: DEFAULT_HEADERS,
@@ -126,7 +165,7 @@ async function createIssue(projectId, options) {
126
165
  return GitLabIssueSchema.parse(data);
127
166
  }
128
167
  async function createMergeRequest(projectId, options) {
129
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests`);
168
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`);
130
169
  const response = await fetch(url.toString(), {
131
170
  method: "POST",
132
171
  headers: {
@@ -156,7 +195,7 @@ async function createMergeRequest(projectId, options) {
156
195
  }
157
196
  async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath) {
158
197
  const encodedPath = encodeURIComponent(filePath);
159
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
198
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
160
199
  const body = {
161
200
  branch,
162
201
  content,
@@ -193,7 +232,7 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
193
232
  return GitLabCreateUpdateFileResponseSchema.parse(data);
194
233
  }
195
234
  async function createTree(projectId, files, ref) {
196
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/tree`);
235
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree`);
197
236
  if (ref) {
198
237
  url.searchParams.append("ref", ref);
199
238
  }
@@ -224,7 +263,7 @@ async function createTree(projectId, files, ref) {
224
263
  return GitLabTreeSchema.parse(data);
225
264
  }
226
265
  async function createCommit(projectId, message, branch, actions) {
227
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/commits`);
266
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`);
228
267
  const response = await fetch(url.toString(), {
229
268
  method: "POST",
230
269
  headers: {
@@ -255,7 +294,7 @@ async function createCommit(projectId, message, branch, actions) {
255
294
  return GitLabCommitSchema.parse(data);
256
295
  }
257
296
  async function searchProjects(query, page = 1, perPage = 20) {
258
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects`);
297
+ const url = new URL(`${GITLAB_API_URL}/projects`);
259
298
  url.searchParams.append("search", query);
260
299
  url.searchParams.append("page", page.toString());
261
300
  url.searchParams.append("per_page", perPage.toString());
@@ -285,7 +324,7 @@ async function searchProjects(query, page = 1, perPage = 20) {
285
324
  });
286
325
  }
287
326
  async function createRepository(options) {
288
- const response = await fetch(`${GITLAB_API_URL}/api/v4/projects`, {
327
+ const response = await fetch(`${GITLAB_API_URL}/projects`, {
289
328
  method: "POST",
290
329
  headers: {
291
330
  Accept: "application/json",
@@ -310,7 +349,7 @@ async function createRepository(options) {
310
349
  }
311
350
  // MR 조회 함수
312
351
  async function getMergeRequest(projectId, mergeRequestIid) {
313
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
352
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
314
353
  const response = await fetch(url.toString(), {
315
354
  headers: DEFAULT_HEADERS,
316
355
  });
@@ -319,7 +358,7 @@ async function getMergeRequest(projectId, mergeRequestIid) {
319
358
  }
320
359
  // MR 변경사항 조회 함수
321
360
  async function getMergeRequestDiffs(projectId, mergeRequestIid, view) {
322
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/changes`);
361
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/changes`);
323
362
  if (view) {
324
363
  url.searchParams.append("view", view);
325
364
  }
@@ -332,7 +371,7 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, view) {
332
371
  }
333
372
  // MR 업데이트 함수
334
373
  async function updateMergeRequest(projectId, mergeRequestIid, options) {
335
- const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
374
+ const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
336
375
  const response = await fetch(url.toString(), {
337
376
  method: "PUT",
338
377
  headers: DEFAULT_HEADERS,
@@ -352,8 +391,11 @@ noteableIid, body) {
352
391
  headers: DEFAULT_HEADERS,
353
392
  body: JSON.stringify({ body }),
354
393
  });
355
- await handleGitLabError(response);
356
- return await response.json(); // ⚙️ 응답 타입은 GitLab API 문서에 따라 조정 가능, 필요하면 스키마 정의
394
+ if (!response.ok) {
395
+ const errorText = await response.text();
396
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
397
+ }
398
+ return await response.json();
357
399
  }
358
400
  server.setRequestHandler(ListToolsRequestSchema, async () => {
359
401
  return {
@@ -535,22 +577,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
535
577
  };
536
578
  }
537
579
  case "create_note": {
538
- try {
539
- const args = CreateNoteSchema.parse(request.params.arguments);
540
- const { project_id, noteable_type, noteable_iid, body } = args;
541
- const note = await createNote(project_id, noteable_type, noteable_iid, body);
542
- return {
543
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
544
- };
545
- }
546
- catch (error) {
547
- if (error instanceof z.ZodError) {
548
- throw new Error(`Invalid arguments: ${error.errors
549
- .map((e) => `${e.path.join(".")}: ${e.message}`)
550
- .join(", ")}`);
551
- }
552
- throw error;
553
- }
580
+ const args = CreateNoteSchema.parse(request.params.arguments);
581
+ const { project_id, noteable_type, noteable_iid, body } = args;
582
+ const note = await createNote(project_id, noteable_type, noteable_iid, body);
583
+ return {
584
+ content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
585
+ };
554
586
  }
555
587
  default:
556
588
  throw new Error(`Unknown tool: ${request.params.name}`);
@@ -566,9 +598,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
566
598
  }
567
599
  });
568
600
  async function runServer() {
569
- const transport = new StdioServerTransport();
570
- await server.connect(transport);
571
- console.error("GitLab MCP Server running on stdio");
601
+ try {
602
+ console.error("========================");
603
+ console.error(`GitLab MCP Server v${SERVER_VERSION}`);
604
+ console.error(`API URL: ${GITLAB_API_URL}`);
605
+ console.error("========================");
606
+ const transport = new StdioServerTransport();
607
+ await server.connect(transport);
608
+ console.error("GitLab MCP Server running on stdio");
609
+ }
610
+ catch (error) {
611
+ console.error("Error initializing server:", error);
612
+ process.exit(1);
613
+ }
572
614
  }
573
615
  runServer().catch((error) => {
574
616
  console.error("Fatal error in main():", error);
@@ -0,0 +1,54 @@
1
+ /**
2
+ * This test file verifies that the createNote function works correctly
3
+ * with the fixed endpoint URL construction that uses plural resource names
4
+ * (issues instead of issue, merge_requests instead of merge_request).
5
+ */
6
+ import fetch from "node-fetch";
7
+ // GitLab API configuration (replace with actual values when testing)
8
+ const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com";
9
+ const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_TOKEN || "";
10
+ const PROJECT_ID = process.env.PROJECT_ID || "your/project";
11
+ const ISSUE_IID = Number(process.env.ISSUE_IID || "1");
12
+ async function testCreateIssueNote() {
13
+ try {
14
+ // Using plural form "issues" in the URL
15
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(PROJECT_ID)}/issues/${ISSUE_IID}/notes`);
16
+ const response = await fetch(url.toString(), {
17
+ method: "POST",
18
+ headers: {
19
+ Accept: "application/json",
20
+ "Content-Type": "application/json",
21
+ Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
22
+ },
23
+ body: JSON.stringify({ body: "Test note from API - with plural endpoint" }),
24
+ });
25
+ if (!response.ok) {
26
+ const errorBody = await response.text();
27
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
28
+ }
29
+ const data = await response.json();
30
+ console.log("Successfully created note:");
31
+ console.log(JSON.stringify(data, null, 2));
32
+ return true;
33
+ }
34
+ catch (error) {
35
+ console.error("Error creating note:", error);
36
+ return false;
37
+ }
38
+ }
39
+ // Only run the test if executed directly
40
+ if (require.main === module) {
41
+ console.log("Testing note creation with plural 'issues' endpoint...");
42
+ testCreateIssueNote().then(success => {
43
+ if (success) {
44
+ console.log("✅ Test successful!");
45
+ process.exit(0);
46
+ }
47
+ else {
48
+ console.log("❌ Test failed!");
49
+ process.exit(1);
50
+ }
51
+ });
52
+ }
53
+ // Export for use in other tests
54
+ export { testCreateIssueNote };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "MCP server for using the GitLab API",
5
5
  "license": "MIT",
6
6
  "author": "zereight",
@@ -28,6 +28,8 @@
28
28
  "zod-to-json-schema": "^3.23.5"
29
29
  },
30
30
  "devDependencies": {
31
- "typescript": "^5.6.2"
31
+ "@types/node": "^22.13.10",
32
+ "typescript": "^5.8.2",
33
+ "zod": "3.21.4"
32
34
  }
33
35
  }