@zereight/mcp-gitlab 1.0.11 → 1.0.12
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 +79 -37
- package/build/test-note.js +54 -0
- package/package.json +4 -2
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:
|
|
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
|
-
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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
|
-
|
|
356
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.12",
|
|
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
|
-
"
|
|
31
|
+
"@types/node": "^22.13.10",
|
|
32
|
+
"typescript": "^5.8.2",
|
|
33
|
+
"zod": "3.21.4"
|
|
32
34
|
}
|
|
33
35
|
}
|