@zereight/mcp-gitlab 1.0.0

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 ADDED
@@ -0,0 +1,22 @@
1
+ # @zereight/mcp-gitlab
2
+
3
+ GitLab MCP(Model Context Protocol) 서버입니다.
4
+
5
+ ## 설치 및 실행
6
+
7
+ ```bash
8
+ npx @zereight/mcp-gitlab
9
+ ```
10
+
11
+ ## 환경 변수 설정
12
+
13
+ 서버를 실행하기 전에 다음 환경 변수를 설정해야 합니다:
14
+
15
+ ```bash
16
+ GITLAB_TOKEN=your_gitlab_token
17
+ GITLAB_API_URL=your_gitlab_api_url # 기본값: https://gitlab.com/api/v4
18
+ ```
19
+
20
+ ## 라이선스
21
+
22
+ MIT License
package/build/index.js ADDED
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import fetch from "node-fetch";
6
+ import { z } from "zod";
7
+ import { zodToJsonSchema } from "zod-to-json-schema";
8
+ import { config } from "dotenv";
9
+ import { fileURLToPath } from "url";
10
+ import { dirname, resolve } from "path";
11
+ import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, GitLabMergeRequestDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, } from "./schemas.js";
12
+ // Get the directory path of the current module
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ // Load environment variables from .env file
16
+ const result = config({ path: resolve(dirname(__dirname), ".env") });
17
+ if (result.error) {
18
+ console.error("Error loading .env file:", result.error);
19
+ process.exit(1);
20
+ }
21
+ const server = new Server({
22
+ name: "gitlab-mcp-server",
23
+ version: "0.5.1",
24
+ }, {
25
+ capabilities: {
26
+ tools: {},
27
+ },
28
+ });
29
+ const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
30
+ const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com/api/v4";
31
+ if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
32
+ console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
33
+ process.exit(1);
34
+ }
35
+ // GitLab API 공통 헤더
36
+ const DEFAULT_HEADERS = {
37
+ Accept: "application/json",
38
+ "Content-Type": "application/json",
39
+ Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
40
+ };
41
+ // API 에러 처리를 위한 유틸리티 함수
42
+ async function handleGitLabError(response) {
43
+ if (!response.ok) {
44
+ const errorBody = await response.text();
45
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
46
+ }
47
+ }
48
+ // 프로젝트 포크 생성
49
+ async function forkProject(projectId, namespace) {
50
+ // API 엔드포인트 URL 생성
51
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/fork`);
52
+ if (namespace) {
53
+ url.searchParams.append("namespace", namespace);
54
+ }
55
+ const response = await fetch(url.toString(), {
56
+ method: "POST",
57
+ headers: DEFAULT_HEADERS,
58
+ });
59
+ // 이미 존재하는 프로젝트인 경우 처리
60
+ if (response.status === 409) {
61
+ throw new Error("Project already exists in the target namespace");
62
+ }
63
+ await handleGitLabError(response);
64
+ const data = await response.json();
65
+ return GitLabForkSchema.parse(data);
66
+ }
67
+ // 새로운 브랜치 생성
68
+ async function createBranch(projectId, options) {
69
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/branches`);
70
+ const response = await fetch(url.toString(), {
71
+ method: "POST",
72
+ headers: DEFAULT_HEADERS,
73
+ body: JSON.stringify({
74
+ branch: options.name,
75
+ ref: options.ref,
76
+ }),
77
+ });
78
+ await handleGitLabError(response);
79
+ return GitLabReferenceSchema.parse(await response.json());
80
+ }
81
+ // 프로젝트의 기본 브랜치 조회
82
+ async function getDefaultBranchRef(projectId) {
83
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}`);
84
+ const response = await fetch(url.toString(), {
85
+ headers: DEFAULT_HEADERS,
86
+ });
87
+ await handleGitLabError(response);
88
+ const project = GitLabRepositorySchema.parse(await response.json());
89
+ return project.default_branch ?? "main";
90
+ }
91
+ // 파일 내용 조회
92
+ async function getFileContents(projectId, filePath, ref) {
93
+ const encodedPath = encodeURIComponent(filePath);
94
+ // ref가 없는 경우 default branch를 가져옴
95
+ if (!ref) {
96
+ ref = await getDefaultBranchRef(projectId);
97
+ }
98
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
99
+ url.searchParams.append("ref", ref);
100
+ const response = await fetch(url.toString(), {
101
+ headers: DEFAULT_HEADERS,
102
+ });
103
+ // 파일을 찾을 수 없는 경우 처리
104
+ if (response.status === 404) {
105
+ throw new Error(`File not found: ${filePath}`);
106
+ }
107
+ await handleGitLabError(response);
108
+ const data = await response.json();
109
+ const parsedData = GitLabContentSchema.parse(data);
110
+ // Base64로 인코딩된 파일 내용을 UTF-8로 디코딩
111
+ if (!Array.isArray(parsedData) && parsedData.content) {
112
+ parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8");
113
+ parsedData.encoding = "utf8";
114
+ }
115
+ return parsedData;
116
+ }
117
+ // 이슈 생성
118
+ async function createIssue(projectId, options) {
119
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/issues`);
120
+ const response = await fetch(url.toString(), {
121
+ method: "POST",
122
+ headers: DEFAULT_HEADERS,
123
+ body: JSON.stringify({
124
+ title: options.title,
125
+ description: options.description,
126
+ assignee_ids: options.assignee_ids,
127
+ milestone_id: options.milestone_id,
128
+ labels: options.labels?.join(","),
129
+ }),
130
+ });
131
+ // 잘못된 요청 처리
132
+ if (response.status === 400) {
133
+ const errorBody = await response.text();
134
+ throw new Error(`Invalid request: ${errorBody}`);
135
+ }
136
+ await handleGitLabError(response);
137
+ const data = await response.json();
138
+ return GitLabIssueSchema.parse(data);
139
+ }
140
+ async function createMergeRequest(projectId, options) {
141
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests`);
142
+ const response = await fetch(url.toString(), {
143
+ method: "POST",
144
+ headers: {
145
+ Accept: "application/json",
146
+ "Content-Type": "application/json",
147
+ Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
148
+ },
149
+ body: JSON.stringify({
150
+ title: options.title,
151
+ description: options.description,
152
+ source_branch: options.source_branch,
153
+ target_branch: options.target_branch,
154
+ allow_collaboration: options.allow_collaboration,
155
+ draft: options.draft,
156
+ }),
157
+ });
158
+ if (response.status === 400) {
159
+ const errorBody = await response.text();
160
+ throw new Error(`Invalid request: ${errorBody}`);
161
+ }
162
+ if (!response.ok) {
163
+ const errorBody = await response.text();
164
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
165
+ }
166
+ const data = await response.json();
167
+ return GitLabMergeRequestSchema.parse(data);
168
+ }
169
+ async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath) {
170
+ const encodedPath = encodeURIComponent(filePath);
171
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
172
+ const body = {
173
+ branch,
174
+ content,
175
+ commit_message: commitMessage,
176
+ encoding: "text",
177
+ ...(previousPath ? { previous_path: previousPath } : {}),
178
+ };
179
+ // Check if file exists
180
+ let method = "POST";
181
+ try {
182
+ await getFileContents(projectId, filePath, branch);
183
+ method = "PUT";
184
+ }
185
+ catch (error) {
186
+ if (!(error instanceof Error && error.message.includes("File not found"))) {
187
+ throw error;
188
+ }
189
+ // File doesn't exist, use POST
190
+ }
191
+ const response = await fetch(url.toString(), {
192
+ method,
193
+ headers: {
194
+ Accept: "application/json",
195
+ "Content-Type": "application/json",
196
+ Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
197
+ },
198
+ body: JSON.stringify(body),
199
+ });
200
+ if (!response.ok) {
201
+ const errorBody = await response.text();
202
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
203
+ }
204
+ const data = await response.json();
205
+ return GitLabCreateUpdateFileResponseSchema.parse(data);
206
+ }
207
+ async function createTree(projectId, files, ref) {
208
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/tree`);
209
+ if (ref) {
210
+ url.searchParams.append("ref", ref);
211
+ }
212
+ const response = await fetch(url.toString(), {
213
+ method: "POST",
214
+ headers: {
215
+ Accept: "application/json",
216
+ "Content-Type": "application/json",
217
+ Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
218
+ },
219
+ body: JSON.stringify({
220
+ files: files.map((file) => ({
221
+ file_path: file.path,
222
+ content: file.content,
223
+ encoding: "text",
224
+ })),
225
+ }),
226
+ });
227
+ if (response.status === 400) {
228
+ const errorBody = await response.text();
229
+ throw new Error(`Invalid request: ${errorBody}`);
230
+ }
231
+ if (!response.ok) {
232
+ const errorBody = await response.text();
233
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
234
+ }
235
+ const data = await response.json();
236
+ return GitLabTreeSchema.parse(data);
237
+ }
238
+ async function createCommit(projectId, message, branch, actions) {
239
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/commits`);
240
+ const response = await fetch(url.toString(), {
241
+ method: "POST",
242
+ headers: {
243
+ Accept: "application/json",
244
+ "Content-Type": "application/json",
245
+ Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
246
+ },
247
+ body: JSON.stringify({
248
+ branch,
249
+ commit_message: message,
250
+ actions: actions.map((action) => ({
251
+ action: "create",
252
+ file_path: action.path,
253
+ content: action.content,
254
+ encoding: "text",
255
+ })),
256
+ }),
257
+ });
258
+ if (response.status === 400) {
259
+ const errorBody = await response.text();
260
+ throw new Error(`Invalid request: ${errorBody}`);
261
+ }
262
+ if (!response.ok) {
263
+ const errorBody = await response.text();
264
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
265
+ }
266
+ const data = await response.json();
267
+ return GitLabCommitSchema.parse(data);
268
+ }
269
+ async function searchProjects(query, page = 1, perPage = 20) {
270
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects`);
271
+ url.searchParams.append("search", query);
272
+ url.searchParams.append("page", page.toString());
273
+ url.searchParams.append("per_page", perPage.toString());
274
+ url.searchParams.append("order_by", "id");
275
+ url.searchParams.append("sort", "desc");
276
+ const response = await fetch(url.toString(), {
277
+ headers: {
278
+ Accept: "application/json",
279
+ "Content-Type": "application/json",
280
+ Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
281
+ },
282
+ });
283
+ if (!response.ok) {
284
+ const errorBody = await response.text();
285
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
286
+ }
287
+ const projects = (await response.json());
288
+ const totalCount = response.headers.get("x-total");
289
+ const totalPages = response.headers.get("x-total-pages");
290
+ // GitLab API doesn't return these headers for results > 10,000
291
+ const count = totalCount ? parseInt(totalCount) : projects.length;
292
+ return GitLabSearchResponseSchema.parse({
293
+ count,
294
+ total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
295
+ current_page: page,
296
+ items: projects,
297
+ });
298
+ }
299
+ async function createRepository(options) {
300
+ const response = await fetch(`${GITLAB_API_URL}/api/v4/projects`, {
301
+ method: "POST",
302
+ headers: {
303
+ Accept: "application/json",
304
+ "Content-Type": "application/json",
305
+ Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
306
+ },
307
+ body: JSON.stringify({
308
+ name: options.name,
309
+ description: options.description,
310
+ visibility: options.visibility,
311
+ initialize_with_readme: options.initialize_with_readme,
312
+ default_branch: "main",
313
+ path: options.name.toLowerCase().replace(/\s+/g, "-"),
314
+ }),
315
+ });
316
+ if (!response.ok) {
317
+ const errorBody = await response.text();
318
+ throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
319
+ }
320
+ const data = await response.json();
321
+ return GitLabRepositorySchema.parse(data);
322
+ }
323
+ // MR 조회 함수
324
+ async function getMergeRequest(projectId, mergeRequestIid) {
325
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
326
+ const response = await fetch(url.toString(), {
327
+ headers: DEFAULT_HEADERS,
328
+ });
329
+ await handleGitLabError(response);
330
+ return GitLabMergeRequestSchema.parse(await response.json());
331
+ }
332
+ // MR 변경사항 조회 함수
333
+ async function getMergeRequestDiffs(projectId, mergeRequestIid, view) {
334
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/changes`);
335
+ if (view) {
336
+ url.searchParams.append("view", view);
337
+ }
338
+ const response = await fetch(url.toString(), {
339
+ headers: DEFAULT_HEADERS,
340
+ });
341
+ await handleGitLabError(response);
342
+ const data = (await response.json());
343
+ return z.array(GitLabMergeRequestDiffSchema).parse(data.changes);
344
+ }
345
+ // MR 업데이트 함수
346
+ async function updateMergeRequest(projectId, mergeRequestIid, options) {
347
+ const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
348
+ const response = await fetch(url.toString(), {
349
+ method: "PUT",
350
+ headers: DEFAULT_HEADERS,
351
+ body: JSON.stringify(options),
352
+ });
353
+ await handleGitLabError(response);
354
+ return GitLabMergeRequestSchema.parse(await response.json());
355
+ }
356
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
357
+ return {
358
+ tools: [
359
+ {
360
+ name: "create_or_update_file",
361
+ description: "Create or update a single file in a GitLab project",
362
+ inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
363
+ },
364
+ {
365
+ name: "search_repositories",
366
+ description: "Search for GitLab projects",
367
+ inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
368
+ },
369
+ {
370
+ name: "create_repository",
371
+ description: "Create a new GitLab project",
372
+ inputSchema: zodToJsonSchema(CreateRepositorySchema),
373
+ },
374
+ {
375
+ name: "get_file_contents",
376
+ description: "Get the contents of a file or directory from a GitLab project",
377
+ inputSchema: zodToJsonSchema(GetFileContentsSchema),
378
+ },
379
+ {
380
+ name: "push_files",
381
+ description: "Push multiple files to a GitLab project in a single commit",
382
+ inputSchema: zodToJsonSchema(PushFilesSchema),
383
+ },
384
+ {
385
+ name: "create_issue",
386
+ description: "Create a new issue in a GitLab project",
387
+ inputSchema: zodToJsonSchema(CreateIssueSchema),
388
+ },
389
+ {
390
+ name: "create_merge_request",
391
+ description: "Create a new merge request in a GitLab project",
392
+ inputSchema: zodToJsonSchema(CreateMergeRequestSchema),
393
+ },
394
+ {
395
+ name: "fork_repository",
396
+ description: "Fork a GitLab project to your account or specified namespace",
397
+ inputSchema: zodToJsonSchema(ForkRepositorySchema),
398
+ },
399
+ {
400
+ name: "create_branch",
401
+ description: "Create a new branch in a GitLab project",
402
+ inputSchema: zodToJsonSchema(CreateBranchSchema),
403
+ },
404
+ {
405
+ name: "get_merge_request",
406
+ description: "Get details of a merge request",
407
+ inputSchema: zodToJsonSchema(GetMergeRequestSchema),
408
+ },
409
+ {
410
+ name: "get_merge_request_diffs",
411
+ description: "Get the changes/diffs of a merge request",
412
+ inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
413
+ },
414
+ {
415
+ name: "update_merge_request",
416
+ description: "Update a merge request",
417
+ inputSchema: zodToJsonSchema(UpdateMergeRequestSchema),
418
+ },
419
+ ],
420
+ };
421
+ });
422
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
423
+ try {
424
+ if (!request.params.arguments) {
425
+ throw new Error("Arguments are required");
426
+ }
427
+ switch (request.params.name) {
428
+ case "fork_repository": {
429
+ const args = ForkRepositorySchema.parse(request.params.arguments);
430
+ const fork = await forkProject(args.project_id, args.namespace);
431
+ return {
432
+ content: [{ type: "text", text: JSON.stringify(fork, null, 2) }],
433
+ };
434
+ }
435
+ case "create_branch": {
436
+ const args = CreateBranchSchema.parse(request.params.arguments);
437
+ let ref = args.ref;
438
+ if (!ref) {
439
+ ref = await getDefaultBranchRef(args.project_id);
440
+ }
441
+ const branch = await createBranch(args.project_id, {
442
+ name: args.branch,
443
+ ref,
444
+ });
445
+ return {
446
+ content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
447
+ };
448
+ }
449
+ case "search_repositories": {
450
+ const args = SearchRepositoriesSchema.parse(request.params.arguments);
451
+ const results = await searchProjects(args.search, args.page, args.per_page);
452
+ return {
453
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
454
+ };
455
+ }
456
+ case "create_repository": {
457
+ const args = CreateRepositorySchema.parse(request.params.arguments);
458
+ const repository = await createRepository(args);
459
+ return {
460
+ content: [
461
+ { type: "text", text: JSON.stringify(repository, null, 2) },
462
+ ],
463
+ };
464
+ }
465
+ case "get_file_contents": {
466
+ const args = GetFileContentsSchema.parse(request.params.arguments);
467
+ const contents = await getFileContents(args.project_id, args.file_path, args.ref);
468
+ return {
469
+ content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
470
+ };
471
+ }
472
+ case "create_or_update_file": {
473
+ const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
474
+ const result = await createOrUpdateFile(args.project_id, args.file_path, args.content, args.commit_message, args.branch, args.previous_path);
475
+ return {
476
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
477
+ };
478
+ }
479
+ case "push_files": {
480
+ const args = PushFilesSchema.parse(request.params.arguments);
481
+ const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map((f) => ({ path: f.file_path, content: f.content })));
482
+ return {
483
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
484
+ };
485
+ }
486
+ case "create_issue": {
487
+ const args = CreateIssueSchema.parse(request.params.arguments);
488
+ const { project_id, ...options } = args;
489
+ const issue = await createIssue(project_id, options);
490
+ return {
491
+ content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
492
+ };
493
+ }
494
+ case "create_merge_request": {
495
+ const args = CreateMergeRequestSchema.parse(request.params.arguments);
496
+ const { project_id, ...options } = args;
497
+ const mergeRequest = await createMergeRequest(project_id, options);
498
+ return {
499
+ content: [
500
+ { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
501
+ ],
502
+ };
503
+ }
504
+ case "get_merge_request": {
505
+ const args = GetMergeRequestSchema.parse(request.params.arguments);
506
+ const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid);
507
+ return {
508
+ content: [
509
+ { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
510
+ ],
511
+ };
512
+ }
513
+ case "get_merge_request_diffs": {
514
+ const args = GetMergeRequestDiffsSchema.parse(request.params.arguments);
515
+ const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.view);
516
+ return {
517
+ content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
518
+ };
519
+ }
520
+ case "update_merge_request": {
521
+ const args = UpdateMergeRequestSchema.parse(request.params.arguments);
522
+ const { project_id, merge_request_iid, ...options } = args;
523
+ const mergeRequest = await updateMergeRequest(project_id, merge_request_iid, options);
524
+ return {
525
+ content: [
526
+ { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
527
+ ],
528
+ };
529
+ }
530
+ default:
531
+ throw new Error(`Unknown tool: ${request.params.name}`);
532
+ }
533
+ }
534
+ catch (error) {
535
+ if (error instanceof z.ZodError) {
536
+ throw new Error(`Invalid arguments: ${error.errors
537
+ .map((e) => `${e.path.join(".")}: ${e.message}`)
538
+ .join(", ")}`);
539
+ }
540
+ throw error;
541
+ }
542
+ });
543
+ async function runServer() {
544
+ const transport = new StdioServerTransport();
545
+ await server.connect(transport);
546
+ console.error("GitLab MCP Server running on stdio");
547
+ }
548
+ runServer().catch((error) => {
549
+ console.error("Fatal error in main():", error);
550
+ process.exit(1);
551
+ });
@@ -0,0 +1,348 @@
1
+ import { z } from "zod";
2
+ // Base schemas for common types
3
+ export const GitLabAuthorSchema = z.object({
4
+ name: z.string(),
5
+ email: z.string(),
6
+ date: z.string(),
7
+ });
8
+ // Repository related schemas
9
+ export const GitLabOwnerSchema = z.object({
10
+ username: z.string(), // Changed from login to match GitLab API
11
+ id: z.number(),
12
+ avatar_url: z.string(),
13
+ web_url: z.string(), // Changed from html_url to match GitLab API
14
+ name: z.string(), // Added as GitLab includes full name
15
+ state: z.string(), // Added as GitLab includes user state
16
+ });
17
+ export const GitLabRepositorySchema = z.object({
18
+ id: z.number(),
19
+ name: z.string(),
20
+ path_with_namespace: z.string(),
21
+ visibility: z.string().optional(),
22
+ owner: GitLabOwnerSchema.optional(),
23
+ web_url: z.string().optional(),
24
+ description: z.string().nullable(),
25
+ fork: z.boolean().optional(),
26
+ ssh_url_to_repo: z.string().optional(),
27
+ http_url_to_repo: z.string().optional(),
28
+ created_at: z.string().optional(),
29
+ last_activity_at: z.string().optional(),
30
+ default_branch: z.string().optional(),
31
+ });
32
+ // File content schemas
33
+ export const GitLabFileContentSchema = z.object({
34
+ file_name: z.string(), // Changed from name to match GitLab API
35
+ file_path: z.string(), // Changed from path to match GitLab API
36
+ size: z.number(),
37
+ encoding: z.string(),
38
+ content: z.string(),
39
+ content_sha256: z.string(), // Changed from sha to match GitLab API
40
+ ref: z.string(), // Added as GitLab requires branch reference
41
+ blob_id: z.string(), // Added to match GitLab API
42
+ last_commit_id: z.string(), // Added to match GitLab API
43
+ });
44
+ export const GitLabDirectoryContentSchema = z.object({
45
+ name: z.string(),
46
+ path: z.string(),
47
+ type: z.string(),
48
+ mode: z.string(),
49
+ id: z.string(), // Changed from sha to match GitLab API
50
+ web_url: z.string(), // Changed from html_url to match GitLab API
51
+ });
52
+ export const GitLabContentSchema = z.union([
53
+ GitLabFileContentSchema,
54
+ z.array(GitLabDirectoryContentSchema),
55
+ ]);
56
+ // Operation schemas
57
+ export const FileOperationSchema = z.object({
58
+ path: z.string(),
59
+ content: z.string(),
60
+ });
61
+ // Tree and commit schemas
62
+ export const GitLabTreeEntrySchema = z.object({
63
+ id: z.string(), // Changed from sha to match GitLab API
64
+ name: z.string(),
65
+ type: z.enum(["blob", "tree"]),
66
+ path: z.string(),
67
+ mode: z.string(),
68
+ });
69
+ export const GitLabTreeSchema = z.object({
70
+ id: z.string(), // Changed from sha to match GitLab API
71
+ tree: z.array(GitLabTreeEntrySchema),
72
+ });
73
+ export const GitLabCommitSchema = z.object({
74
+ id: z.string(), // Changed from sha to match GitLab API
75
+ short_id: z.string(), // Added to match GitLab API
76
+ title: z.string(), // Changed from message to match GitLab API
77
+ author_name: z.string(),
78
+ author_email: z.string(),
79
+ authored_date: z.string(),
80
+ committer_name: z.string(),
81
+ committer_email: z.string(),
82
+ committed_date: z.string(),
83
+ web_url: z.string(), // Changed from html_url to match GitLab API
84
+ parent_ids: z.array(z.string()), // Changed from parents to match GitLab API
85
+ });
86
+ // Reference schema
87
+ export const GitLabReferenceSchema = z.object({
88
+ name: z.string(), // Changed from ref to match GitLab API
89
+ commit: z.object({
90
+ id: z.string(), // Changed from sha to match GitLab API
91
+ web_url: z.string(), // Changed from url to match GitLab API
92
+ }),
93
+ });
94
+ // Input schemas for operations
95
+ export const CreateRepositoryOptionsSchema = z.object({
96
+ name: z.string(),
97
+ description: z.string().optional(),
98
+ visibility: z.enum(["private", "internal", "public"]).optional(), // Changed from private to match GitLab API
99
+ initialize_with_readme: z.boolean().optional(), // Changed from auto_init to match GitLab API
100
+ });
101
+ export const CreateIssueOptionsSchema = z.object({
102
+ title: z.string(),
103
+ description: z.string().optional(), // Changed from body to match GitLab API
104
+ assignee_ids: z.array(z.number()).optional(), // Changed from assignees to match GitLab API
105
+ milestone_id: z.number().optional(), // Changed from milestone to match GitLab API
106
+ labels: z.array(z.string()).optional(),
107
+ });
108
+ export const CreateMergeRequestOptionsSchema = z.object({
109
+ // Changed from CreatePullRequestOptionsSchema
110
+ title: z.string(),
111
+ description: z.string().optional(), // Changed from body to match GitLab API
112
+ source_branch: z.string(), // Changed from head to match GitLab API
113
+ target_branch: z.string(), // Changed from base to match GitLab API
114
+ allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API
115
+ draft: z.boolean().optional(),
116
+ });
117
+ export const CreateBranchOptionsSchema = z.object({
118
+ name: z.string(), // Changed from ref to match GitLab API
119
+ ref: z.string(), // The source branch/commit for the new branch
120
+ });
121
+ // Response schemas for operations
122
+ export const GitLabCreateUpdateFileResponseSchema = z.object({
123
+ file_path: z.string(),
124
+ branch: z.string(),
125
+ commit_id: z.string(), // Changed from sha to match GitLab API
126
+ content: GitLabFileContentSchema.optional(),
127
+ });
128
+ export const GitLabSearchResponseSchema = z.object({
129
+ count: z.number().optional(),
130
+ total_pages: z.number().optional(),
131
+ current_page: z.number().optional(),
132
+ items: z.array(GitLabRepositorySchema),
133
+ });
134
+ // Fork related schemas
135
+ export const GitLabForkParentSchema = z.object({
136
+ name: z.string(),
137
+ path_with_namespace: z.string(), // Changed from full_name to match GitLab API
138
+ owner: z.object({
139
+ username: z.string(), // Changed from login to match GitLab API
140
+ id: z.number(),
141
+ avatar_url: z.string(),
142
+ }),
143
+ web_url: z.string(), // Changed from html_url to match GitLab API
144
+ });
145
+ export const GitLabForkSchema = GitLabRepositorySchema.extend({
146
+ forked_from_project: GitLabForkParentSchema, // Changed from parent to match GitLab API
147
+ });
148
+ // Issue related schemas
149
+ export const GitLabLabelSchema = z.object({
150
+ id: z.number(),
151
+ name: z.string(),
152
+ color: z.string(),
153
+ description: z.string().optional(),
154
+ });
155
+ export const GitLabUserSchema = z.object({
156
+ username: z.string(), // Changed from login to match GitLab API
157
+ id: z.number(),
158
+ name: z.string(),
159
+ avatar_url: z.string(),
160
+ web_url: z.string(), // Changed from html_url to match GitLab API
161
+ });
162
+ export const GitLabMilestoneSchema = z.object({
163
+ id: z.number(),
164
+ iid: z.number(), // Added to match GitLab API
165
+ title: z.string(),
166
+ description: z.string(),
167
+ state: z.string(),
168
+ web_url: z.string(), // Changed from html_url to match GitLab API
169
+ });
170
+ export const GitLabIssueSchema = z.object({
171
+ id: z.number(),
172
+ iid: z.number(), // Added to match GitLab API
173
+ project_id: z.number(), // Added to match GitLab API
174
+ title: z.string(),
175
+ description: z.string(), // Changed from body to match GitLab API
176
+ state: z.string(),
177
+ author: GitLabUserSchema,
178
+ assignees: z.array(GitLabUserSchema),
179
+ labels: z.array(GitLabLabelSchema),
180
+ milestone: GitLabMilestoneSchema.nullable(),
181
+ created_at: z.string(),
182
+ updated_at: z.string(),
183
+ closed_at: z.string().nullable(),
184
+ web_url: z.string(), // Changed from html_url to match GitLab API
185
+ });
186
+ // Merge Request related schemas (equivalent to Pull Request)
187
+ export const GitLabMergeRequestDiffRefSchema = z.object({
188
+ base_sha: z.string(),
189
+ head_sha: z.string(),
190
+ start_sha: z.string(),
191
+ });
192
+ export const GitLabMergeRequestSchema = z.object({
193
+ id: z.number(),
194
+ iid: z.number(),
195
+ project_id: z.number(),
196
+ title: z.string(),
197
+ description: z.string().nullable(),
198
+ state: z.string(),
199
+ merged: z.boolean().optional(),
200
+ draft: z.boolean().optional(),
201
+ author: GitLabUserSchema,
202
+ assignees: z.array(GitLabUserSchema).optional(),
203
+ source_branch: z.string(),
204
+ target_branch: z.string(),
205
+ diff_refs: GitLabMergeRequestDiffRefSchema.optional(),
206
+ web_url: z.string(),
207
+ created_at: z.string(),
208
+ updated_at: z.string(),
209
+ merged_at: z.string().nullable(),
210
+ closed_at: z.string().nullable(),
211
+ merge_commit_sha: z.string().nullable(),
212
+ detailed_merge_status: z.string().optional(),
213
+ merge_status: z.string().optional(),
214
+ merge_error: z.string().nullable().optional(),
215
+ work_in_progress: z.boolean().optional(),
216
+ blocking_discussions_resolved: z.boolean().optional(),
217
+ should_remove_source_branch: z.boolean().nullable().optional(),
218
+ force_remove_source_branch: z.boolean().optional(),
219
+ allow_collaboration: z.boolean().optional(),
220
+ allow_maintainer_to_push: z.boolean().optional(),
221
+ changes_count: z.string().optional(),
222
+ merge_when_pipeline_succeeds: z.boolean().optional(),
223
+ squash: z.boolean().optional(),
224
+ labels: z.array(z.string()).optional(),
225
+ });
226
+ // API Operation Parameter Schemas
227
+ const ProjectParamsSchema = z.object({
228
+ project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API
229
+ });
230
+ export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
231
+ file_path: z.string().describe("Path where to create/update the file"),
232
+ content: z.string().describe("Content of the file"),
233
+ commit_message: z.string().describe("Commit message"),
234
+ branch: z.string().describe("Branch to create/update the file in"),
235
+ previous_path: z
236
+ .string()
237
+ .optional()
238
+ .describe("Path of the file to move/rename"),
239
+ });
240
+ export const SearchRepositoriesSchema = z.object({
241
+ search: z.string().describe("Search query"), // Changed from query to match GitLab API
242
+ page: z
243
+ .number()
244
+ .optional()
245
+ .describe("Page number for pagination (default: 1)"),
246
+ per_page: z
247
+ .number()
248
+ .optional()
249
+ .describe("Number of results per page (default: 20)"),
250
+ });
251
+ export const CreateRepositorySchema = z.object({
252
+ name: z.string().describe("Repository name"),
253
+ description: z.string().optional().describe("Repository description"),
254
+ visibility: z
255
+ .enum(["private", "internal", "public"])
256
+ .optional()
257
+ .describe("Repository visibility level"),
258
+ initialize_with_readme: z
259
+ .boolean()
260
+ .optional()
261
+ .describe("Initialize with README.md"),
262
+ });
263
+ export const GetFileContentsSchema = ProjectParamsSchema.extend({
264
+ file_path: z.string().describe("Path to the file or directory"),
265
+ ref: z.string().optional().describe("Branch/tag/commit to get contents from"),
266
+ });
267
+ export const PushFilesSchema = ProjectParamsSchema.extend({
268
+ branch: z.string().describe("Branch to push to"),
269
+ files: z
270
+ .array(z.object({
271
+ file_path: z.string().describe("Path where to create the file"),
272
+ content: z.string().describe("Content of the file"),
273
+ }))
274
+ .describe("Array of files to push"),
275
+ commit_message: z.string().describe("Commit message"),
276
+ });
277
+ export const CreateIssueSchema = ProjectParamsSchema.extend({
278
+ title: z.string().describe("Issue title"),
279
+ description: z.string().optional().describe("Issue description"),
280
+ assignee_ids: z
281
+ .array(z.number())
282
+ .optional()
283
+ .describe("Array of user IDs to assign"),
284
+ labels: z.array(z.string()).optional().describe("Array of label names"),
285
+ milestone_id: z.number().optional().describe("Milestone ID to assign"),
286
+ });
287
+ export const CreateMergeRequestSchema = ProjectParamsSchema.extend({
288
+ title: z.string().describe("Merge request title"),
289
+ description: z.string().optional().describe("Merge request description"),
290
+ source_branch: z.string().describe("Branch containing changes"),
291
+ target_branch: z.string().describe("Branch to merge into"),
292
+ draft: z.boolean().optional().describe("Create as draft merge request"),
293
+ allow_collaboration: z
294
+ .boolean()
295
+ .optional()
296
+ .describe("Allow commits from upstream members"),
297
+ });
298
+ export const ForkRepositorySchema = ProjectParamsSchema.extend({
299
+ namespace: z.string().optional().describe("Namespace to fork to (full path)"),
300
+ });
301
+ export const CreateBranchSchema = ProjectParamsSchema.extend({
302
+ branch: z.string().describe("Name for the new branch"),
303
+ ref: z.string().optional().describe("Source branch/commit for new branch"),
304
+ });
305
+ export const GitLabMergeRequestDiffSchema = z.object({
306
+ old_path: z.string(),
307
+ new_path: z.string(),
308
+ a_mode: z.string(),
309
+ b_mode: z.string(),
310
+ diff: z.string(),
311
+ new_file: z.boolean(),
312
+ renamed_file: z.boolean(),
313
+ deleted_file: z.boolean(),
314
+ });
315
+ export const GetMergeRequestSchema = ProjectParamsSchema.extend({
316
+ merge_request_iid: z
317
+ .number()
318
+ .describe("The internal ID of the merge request"),
319
+ });
320
+ export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
321
+ title: z.string().optional().describe("The title of the merge request"),
322
+ description: z
323
+ .string()
324
+ .optional()
325
+ .describe("The description of the merge request"),
326
+ target_branch: z.string().optional().describe("The target branch"),
327
+ assignee_ids: z
328
+ .array(z.number())
329
+ .optional()
330
+ .describe("The ID of the users to assign the MR to"),
331
+ labels: z.array(z.string()).optional().describe("Labels for the MR"),
332
+ state_event: z
333
+ .enum(["close", "reopen"])
334
+ .optional()
335
+ .describe("New state (close/reopen) for the MR"),
336
+ remove_source_branch: z
337
+ .boolean()
338
+ .optional()
339
+ .describe("Flag indicating if the source branch should be removed"),
340
+ squash: z
341
+ .boolean()
342
+ .optional()
343
+ .describe("Squash commits into a single commit when merging"),
344
+ draft: z.boolean().optional().describe("Work in progress merge request"),
345
+ });
346
+ export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
347
+ view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
348
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@zereight/mcp-gitlab",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for using the GitLab API",
5
+ "license": "MIT",
6
+ "author": "zereight",
7
+ "type": "module",
8
+ "bin": {
9
+ "mcp-gitlab": "build/index.js"
10
+ },
11
+ "files": [
12
+ "build"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "engines": {
18
+ "node": ">=14"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
22
+ "prepare": "npm run build",
23
+ "watch": "tsc --watch"
24
+ },
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "1.0.1",
27
+ "@types/node-fetch": "^2.6.12",
28
+ "dotenv": "^16.4.7",
29
+ "node-fetch": "^3.3.2",
30
+ "zod-to-json-schema": "^3.23.5"
31
+ },
32
+ "devDependencies": {
33
+ "shx": "^0.3.4",
34
+ "typescript": "^5.6.2"
35
+ }
36
+ }