gitlab-mcp 0.1.5 → 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.
Files changed (55) hide show
  1. package/.dockerignore +7 -0
  2. package/.editorconfig +9 -0
  3. package/.env.example +75 -0
  4. package/.github/workflows/nodejs.yml +31 -0
  5. package/.github/workflows/npm-publish.yml +31 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.nvmrc +1 -0
  8. package/.prettierrc.json +6 -0
  9. package/Dockerfile +20 -0
  10. package/README.md +416 -251
  11. package/docker-compose.yml +10 -0
  12. package/docs/architecture.md +310 -0
  13. package/docs/authentication.md +299 -0
  14. package/docs/configuration.md +149 -0
  15. package/docs/deployment.md +336 -0
  16. package/docs/tools.md +294 -0
  17. package/eslint.config.js +23 -0
  18. package/package.json +70 -32
  19. package/scripts/get-oauth-token.example.sh +15 -0
  20. package/src/config/env.ts +171 -0
  21. package/src/http.ts +605 -0
  22. package/src/index.ts +77 -0
  23. package/src/lib/auth-context.ts +19 -0
  24. package/src/lib/gitlab-client.ts +1810 -0
  25. package/src/lib/logger.ts +17 -0
  26. package/src/lib/network.ts +45 -0
  27. package/src/lib/oauth.ts +287 -0
  28. package/src/lib/output.ts +51 -0
  29. package/src/lib/policy.ts +78 -0
  30. package/src/lib/request-runtime.ts +376 -0
  31. package/src/lib/sanitize.ts +25 -0
  32. package/src/server/build-server.ts +17 -0
  33. package/src/tools/gitlab.ts +3128 -0
  34. package/src/tools/health.ts +27 -0
  35. package/src/tools/mr-code-context.ts +473 -0
  36. package/src/types/context.ts +13 -0
  37. package/tests/auth-context.test.ts +102 -0
  38. package/tests/gitlab-client.test.ts +674 -0
  39. package/tests/graphql-guard.test.ts +121 -0
  40. package/tests/integration/agent-loop.integration.test.ts +552 -0
  41. package/tests/integration/server.integration.test.ts +543 -0
  42. package/tests/mr-code-context.test.ts +600 -0
  43. package/tests/oauth.test.ts +43 -0
  44. package/tests/output.test.ts +186 -0
  45. package/tests/policy.test.ts +324 -0
  46. package/tests/request-runtime.test.ts +252 -0
  47. package/tests/sanitize.test.ts +123 -0
  48. package/tests/upload-reference.test.ts +84 -0
  49. package/tsconfig.build.json +11 -0
  50. package/tsconfig.json +21 -0
  51. package/vitest.config.ts +12 -0
  52. package/LICENSE +0 -21
  53. package/build/index.js +0 -1642
  54. package/build/schemas.js +0 -684
  55. package/build/test-note.js +0 -54
package/build/index.js DELETED
@@ -1,1642 +0,0 @@
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 { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
6
- import fetch from "node-fetch";
7
- import { z } from "zod";
8
- import { zodToJsonSchema } from "zod-to-json-schema";
9
- import { fileURLToPath } from "url";
10
- import { dirname } from "path";
11
- import fs from "fs";
12
- import path from "path";
13
- import { config } from "dotenv";
14
- import yargs from "yargs";
15
- import { hideBin } from "yargs/helpers";
16
- import express from "express";
17
- 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,
18
- // Discussion Schemas
19
- GitLabDiscussionNoteSchema, // Added
20
- GitLabDiscussionSchema, UpdateMergeRequestNoteSchema, // Added
21
- ListMergeRequestDiscussionsSchema, } from "./schemas.js";
22
- const argv = yargs(hideBin(process.argv)).argv;
23
- const isSSE = argv.mode === "sse";
24
- const port = argv.port ?? 3044;
25
- const envFile = argv.envFile ?? ".env";
26
- // Load .env from the current working directory
27
- config({ path: envFile ?? path.resolve(process.cwd(), ".env") });
28
- /**
29
- * Read version from package.json
30
- */
31
- const __filename = fileURLToPath(import.meta.url);
32
- const __dirname = dirname(__filename);
33
- const packageJsonPath = path.resolve(__dirname, "../package.json");
34
- let SERVER_VERSION = "unknown";
35
- try {
36
- if (fs.existsSync(packageJsonPath)) {
37
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
38
- SERVER_VERSION = packageJson.version || SERVER_VERSION;
39
- }
40
- }
41
- catch (error) {
42
- console.error("Warning: Could not read version from package.json:", error);
43
- }
44
- const server = new Server({
45
- name: "better-gitlab-mcp-server",
46
- version: SERVER_VERSION,
47
- }, {
48
- capabilities: {
49
- tools: {},
50
- },
51
- });
52
- const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
53
- const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
54
- // Define all available tools
55
- const allTools = [
56
- {
57
- name: "create_or_update_file",
58
- description: "Create or update a single file in a GitLab project",
59
- inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
60
- },
61
- {
62
- name: "search_repositories",
63
- description: "Search for GitLab projects",
64
- inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
65
- },
66
- {
67
- name: "create_repository",
68
- description: "Create a new GitLab project",
69
- inputSchema: zodToJsonSchema(CreateRepositorySchema),
70
- },
71
- {
72
- name: "get_file_contents",
73
- description: "Get the contents of a file or directory from a GitLab project",
74
- inputSchema: zodToJsonSchema(GetFileContentsSchema),
75
- },
76
- {
77
- name: "push_files",
78
- description: "Push multiple files to a GitLab project in a single commit",
79
- inputSchema: zodToJsonSchema(PushFilesSchema),
80
- },
81
- {
82
- name: "create_issue",
83
- description: "Create a new issue in a GitLab project",
84
- inputSchema: zodToJsonSchema(CreateIssueSchema),
85
- },
86
- {
87
- name: "create_merge_request",
88
- description: "Create a new merge request in a GitLab project",
89
- inputSchema: zodToJsonSchema(CreateMergeRequestSchema),
90
- },
91
- {
92
- name: "fork_repository",
93
- description: "Fork a GitLab project to your account or specified namespace",
94
- inputSchema: zodToJsonSchema(ForkRepositorySchema),
95
- },
96
- {
97
- name: "create_branch",
98
- description: "Create a new branch in a GitLab project",
99
- inputSchema: zodToJsonSchema(CreateBranchSchema),
100
- },
101
- {
102
- name: "get_merge_request",
103
- description: "Get details of a merge request",
104
- inputSchema: zodToJsonSchema(GetMergeRequestSchema),
105
- },
106
- {
107
- name: "get_merge_request_diffs",
108
- description: "Get the changes/diffs of a merge request",
109
- inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
110
- },
111
- {
112
- name: "update_merge_request",
113
- description: "Update a merge request",
114
- inputSchema: zodToJsonSchema(UpdateMergeRequestSchema),
115
- },
116
- {
117
- name: "create_note",
118
- description: "Create a new note (comment) to an issue or merge request",
119
- inputSchema: zodToJsonSchema(CreateNoteSchema),
120
- },
121
- {
122
- name: "list_merge_request_discussions",
123
- description: "List discussion items for a merge request",
124
- inputSchema: zodToJsonSchema(ListMergeRequestDiscussionsSchema),
125
- },
126
- {
127
- name: "update_merge_request_note",
128
- description: "Modify an existing merge request thread note",
129
- inputSchema: zodToJsonSchema(UpdateMergeRequestNoteSchema),
130
- },
131
- {
132
- name: "list_issues",
133
- description: "List issues in a GitLab project with filtering options",
134
- inputSchema: zodToJsonSchema(ListIssuesSchema),
135
- },
136
- {
137
- name: "get_issue",
138
- description: "Get details of a specific issue in a GitLab project",
139
- inputSchema: zodToJsonSchema(GetIssueSchema),
140
- },
141
- {
142
- name: "update_issue",
143
- description: "Update an issue in a GitLab project",
144
- inputSchema: zodToJsonSchema(UpdateIssueSchema),
145
- },
146
- {
147
- name: "delete_issue",
148
- description: "Delete an issue from a GitLab project",
149
- inputSchema: zodToJsonSchema(DeleteIssueSchema),
150
- },
151
- {
152
- name: "list_issue_links",
153
- description: "List all issue links for a specific issue",
154
- inputSchema: zodToJsonSchema(ListIssueLinksSchema),
155
- },
156
- {
157
- name: "get_issue_link",
158
- description: "Get a specific issue link",
159
- inputSchema: zodToJsonSchema(GetIssueLinkSchema),
160
- },
161
- {
162
- name: "create_issue_link",
163
- description: "Create an issue link between two issues",
164
- inputSchema: zodToJsonSchema(CreateIssueLinkSchema),
165
- },
166
- {
167
- name: "delete_issue_link",
168
- description: "Delete an issue link",
169
- inputSchema: zodToJsonSchema(DeleteIssueLinkSchema),
170
- },
171
- {
172
- name: "list_namespaces",
173
- description: "List all namespaces available to the current user",
174
- inputSchema: zodToJsonSchema(ListNamespacesSchema),
175
- },
176
- {
177
- name: "get_namespace",
178
- description: "Get details of a namespace by ID or path",
179
- inputSchema: zodToJsonSchema(GetNamespaceSchema),
180
- },
181
- {
182
- name: "verify_namespace",
183
- description: "Verify if a namespace path exists",
184
- inputSchema: zodToJsonSchema(VerifyNamespaceSchema),
185
- },
186
- {
187
- name: "get_project",
188
- description: "Get details of a specific project",
189
- inputSchema: zodToJsonSchema(GetProjectSchema),
190
- },
191
- {
192
- name: "list_projects",
193
- description: "List projects accessible by the current user",
194
- inputSchema: zodToJsonSchema(ListProjectsSchema),
195
- },
196
- {
197
- name: "list_labels",
198
- description: "List labels for a project",
199
- inputSchema: zodToJsonSchema(ListLabelsSchema),
200
- },
201
- {
202
- name: "get_label",
203
- description: "Get a single label from a project",
204
- inputSchema: zodToJsonSchema(GetLabelSchema),
205
- },
206
- {
207
- name: "create_label",
208
- description: "Create a new label in a project",
209
- inputSchema: zodToJsonSchema(CreateLabelSchema),
210
- },
211
- {
212
- name: "update_label",
213
- description: "Update an existing label in a project",
214
- inputSchema: zodToJsonSchema(UpdateLabelSchema),
215
- },
216
- {
217
- name: "delete_label",
218
- description: "Delete a label from a project",
219
- inputSchema: zodToJsonSchema(DeleteLabelSchema),
220
- },
221
- {
222
- name: "list_group_projects",
223
- description: "List projects in a GitLab group with filtering options",
224
- inputSchema: zodToJsonSchema(ListGroupProjectsSchema),
225
- },
226
- ];
227
- // Define which tools are read-only
228
- const readOnlyTools = [
229
- "search_repositories",
230
- "get_file_contents",
231
- "get_merge_request",
232
- "get_merge_request_diffs",
233
- "list_merge_request_discussions",
234
- "list_issues",
235
- "get_issue",
236
- "list_issue_links",
237
- "get_issue_link",
238
- "list_namespaces",
239
- "get_namespace",
240
- "verify_namespace",
241
- "get_project",
242
- "list_projects",
243
- "list_labels",
244
- "get_label",
245
- "list_group_projects",
246
- ];
247
- /**
248
- * Smart URL handling for GitLab API
249
- *
250
- * @param {string | undefined} url - Input GitLab API URL
251
- * @returns {string} Normalized GitLab API URL with /api/v4 path
252
- */
253
- function normalizeGitLabApiUrl(url) {
254
- if (!url) {
255
- return "https://gitlab.com/api/v4";
256
- }
257
- // Remove trailing slash if present
258
- let normalizedUrl = url.endsWith("/") ? url.slice(0, -1) : url;
259
- // Check if URL already has /api/v4
260
- if (!normalizedUrl.endsWith("/api/v4") &&
261
- !normalizedUrl.endsWith("/api/v4/")) {
262
- // Append /api/v4 if not already present
263
- normalizedUrl = `${normalizedUrl}/api/v4`;
264
- }
265
- return normalizedUrl;
266
- }
267
- // Use the normalizeGitLabApiUrl function to handle various URL formats
268
- const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || "");
269
- if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
270
- console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
271
- process.exit(1);
272
- }
273
- /**
274
- * Common headers for GitLab API requests
275
- */
276
- const DEFAULT_HEADERS = {
277
- Accept: "application/json",
278
- "Content-Type": "application/json",
279
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
280
- };
281
- /**
282
- * Utility function for handling GitLab API errors
283
- *
284
- * @param {import("node-fetch").Response} response - The response from GitLab API
285
- * @throws {Error} Throws an error with response details if the request failed
286
- */
287
- async function handleGitLabError(response) {
288
- if (!response.ok) {
289
- const errorBody = await response.text();
290
- // Check specifically for Rate Limit error
291
- if (response.status === 403 &&
292
- errorBody.includes("User API Key Rate limit exceeded")) {
293
- console.error("GitLab API Rate Limit Exceeded:", errorBody);
294
- console.log("User API Key Rate limit exceeded. Please try again later.");
295
- throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
296
- }
297
- else {
298
- // Handle other API errors
299
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
300
- }
301
- }
302
- }
303
- /**
304
- * Create a fork of a GitLab project
305
- *
306
- * @param {string} projectId - The ID or URL-encoded path of the project
307
- * @param {string} [namespace] - The namespace to fork the project to
308
- * @returns {Promise<GitLabFork>} The created fork
309
- */
310
- async function forkProject(projectId, namespace) {
311
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`);
312
- if (namespace) {
313
- url.searchParams.append("namespace", namespace);
314
- }
315
- const response = await fetch(url.toString(), {
316
- method: "POST",
317
- headers: DEFAULT_HEADERS,
318
- });
319
- if (response.status === 409) {
320
- throw new Error("Project already exists in the target namespace");
321
- }
322
- await handleGitLabError(response);
323
- const data = await response.json();
324
- return GitLabForkSchema.parse(data);
325
- }
326
- /**
327
- * Create a new branch in a GitLab project
328
- *
329
- * @param {string} projectId - The ID or URL-encoded path of the project
330
- * @param {z.infer<typeof CreateBranchOptionsSchema>} options - Branch creation options
331
- * @returns {Promise<GitLabReference>} The created branch reference
332
- */
333
- async function createBranch(projectId, options) {
334
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches`);
335
- const response = await fetch(url.toString(), {
336
- method: "POST",
337
- headers: DEFAULT_HEADERS,
338
- body: JSON.stringify({
339
- branch: options.name,
340
- ref: options.ref,
341
- }),
342
- });
343
- await handleGitLabError(response);
344
- return GitLabReferenceSchema.parse(await response.json());
345
- }
346
- /**
347
- * Get the default branch for a GitLab project
348
- *
349
- * @param {string} projectId - The ID or URL-encoded path of the project
350
- * @returns {Promise<string>} The name of the default branch
351
- */
352
- async function getDefaultBranchRef(projectId) {
353
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`);
354
- const response = await fetch(url.toString(), {
355
- headers: DEFAULT_HEADERS,
356
- });
357
- await handleGitLabError(response);
358
- const project = GitLabRepositorySchema.parse(await response.json());
359
- return project.default_branch ?? "main";
360
- }
361
- /**
362
- * Get the contents of a file from a GitLab project
363
- *
364
- * @param {string} projectId - The ID or URL-encoded path of the project
365
- * @param {string} filePath - The path of the file to get
366
- * @param {string} [ref] - The name of the branch, tag or commit
367
- * @returns {Promise<GitLabContent>} The file content
368
- */
369
- async function getFileContents(projectId, filePath, ref) {
370
- const encodedPath = encodeURIComponent(filePath);
371
- if (!ref) {
372
- ref = await getDefaultBranchRef(projectId);
373
- }
374
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
375
- url.searchParams.append("ref", ref);
376
- const response = await fetch(url.toString(), {
377
- headers: DEFAULT_HEADERS,
378
- });
379
- if (response.status === 404) {
380
- throw new Error(`File not found: ${filePath}`);
381
- }
382
- await handleGitLabError(response);
383
- const data = await response.json();
384
- const parsedData = GitLabContentSchema.parse(data);
385
- if (!Array.isArray(parsedData) && parsedData.content) {
386
- parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8");
387
- parsedData.encoding = "utf8";
388
- }
389
- return parsedData;
390
- }
391
- /**
392
- * Create a new issue in a GitLab project
393
- *
394
- * @param {string} projectId - The ID or URL-encoded path of the project
395
- * @param {z.infer<typeof CreateIssueOptionsSchema>} options - Issue creation options
396
- * @returns {Promise<GitLabIssue>} The created issue
397
- */
398
- async function createIssue(projectId, options) {
399
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`);
400
- const response = await fetch(url.toString(), {
401
- method: "POST",
402
- headers: DEFAULT_HEADERS,
403
- body: JSON.stringify({
404
- title: options.title,
405
- description: options.description,
406
- assignee_ids: options.assignee_ids,
407
- milestone_id: options.milestone_id,
408
- labels: options.labels?.join(","),
409
- }),
410
- });
411
- if (response.status === 400) {
412
- const errorBody = await response.text();
413
- throw new Error(`Invalid request: ${errorBody}`);
414
- }
415
- await handleGitLabError(response);
416
- const data = await response.json();
417
- return GitLabIssueSchema.parse(data);
418
- }
419
- /**
420
- * List issues in a GitLab project
421
- *
422
- * @param {string} projectId - The ID or URL-encoded path of the project
423
- * @param {Object} options - Options for listing issues
424
- * @returns {Promise<GitLabIssue[]>} List of issues
425
- */
426
- async function listIssues(projectId, options = {}) {
427
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`);
428
- // Add all query parameters
429
- Object.entries(options).forEach(([key, value]) => {
430
- if (value !== undefined) {
431
- if (key === "label_name" && Array.isArray(value)) {
432
- // Handle array of labels
433
- url.searchParams.append(key, value.join(","));
434
- }
435
- else {
436
- url.searchParams.append(key, value.toString());
437
- }
438
- }
439
- });
440
- const response = await fetch(url.toString(), {
441
- headers: DEFAULT_HEADERS,
442
- });
443
- await handleGitLabError(response);
444
- const data = await response.json();
445
- return z.array(GitLabIssueSchema).parse(data);
446
- }
447
- /**
448
- * Get a single issue from a GitLab project
449
- *
450
- * @param {string} projectId - The ID or URL-encoded path of the project
451
- * @param {number} issueIid - The internal ID of the project issue
452
- * @returns {Promise<GitLabIssue>} The issue
453
- */
454
- async function getIssue(projectId, issueIid) {
455
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`);
456
- const response = await fetch(url.toString(), {
457
- headers: DEFAULT_HEADERS,
458
- });
459
- await handleGitLabError(response);
460
- const data = await response.json();
461
- return GitLabIssueSchema.parse(data);
462
- }
463
- /**
464
- * Update an issue in a GitLab project
465
- *
466
- * @param {string} projectId - The ID or URL-encoded path of the project
467
- * @param {number} issueIid - The internal ID of the project issue
468
- * @param {Object} options - Update options for the issue
469
- * @returns {Promise<GitLabIssue>} The updated issue
470
- */
471
- async function updateIssue(projectId, issueIid, options) {
472
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`);
473
- // Convert labels array to comma-separated string if present
474
- const body = { ...options };
475
- if (body.labels && Array.isArray(body.labels)) {
476
- body.labels = body.labels.join(",");
477
- }
478
- const response = await fetch(url.toString(), {
479
- method: "PUT",
480
- headers: DEFAULT_HEADERS,
481
- body: JSON.stringify(body),
482
- });
483
- await handleGitLabError(response);
484
- const data = await response.json();
485
- return GitLabIssueSchema.parse(data);
486
- }
487
- /**
488
- * Delete an issue from a GitLab project
489
- *
490
- * @param {string} projectId - The ID or URL-encoded path of the project
491
- * @param {number} issueIid - The internal ID of the project issue
492
- * @returns {Promise<void>}
493
- */
494
- async function deleteIssue(projectId, issueIid) {
495
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`);
496
- const response = await fetch(url.toString(), {
497
- method: "DELETE",
498
- headers: DEFAULT_HEADERS,
499
- });
500
- await handleGitLabError(response);
501
- }
502
- /**
503
- * List all issue links for a specific issue
504
- *
505
- * @param {string} projectId - The ID or URL-encoded path of the project
506
- * @param {number} issueIid - The internal ID of the project issue
507
- * @returns {Promise<GitLabIssueWithLinkDetails[]>} List of issues with link details
508
- */
509
- async function listIssueLinks(projectId, issueIid) {
510
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links`);
511
- const response = await fetch(url.toString(), {
512
- headers: DEFAULT_HEADERS,
513
- });
514
- await handleGitLabError(response);
515
- const data = await response.json();
516
- return z.array(GitLabIssueWithLinkDetailsSchema).parse(data);
517
- }
518
- /**
519
- * Get a specific issue link
520
- *
521
- * @param {string} projectId - The ID or URL-encoded path of the project
522
- * @param {number} issueIid - The internal ID of the project issue
523
- * @param {number} issueLinkId - The ID of the issue link
524
- * @returns {Promise<GitLabIssueLink>} The issue link
525
- */
526
- async function getIssueLink(projectId, issueIid, issueLinkId) {
527
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}`);
528
- const response = await fetch(url.toString(), {
529
- headers: DEFAULT_HEADERS,
530
- });
531
- await handleGitLabError(response);
532
- const data = await response.json();
533
- return GitLabIssueLinkSchema.parse(data);
534
- }
535
- /**
536
- * Create an issue link between two issues
537
- *
538
- * @param {string} projectId - The ID or URL-encoded path of the project
539
- * @param {number} issueIid - The internal ID of the project issue
540
- * @param {string} targetProjectId - The ID or URL-encoded path of the target project
541
- * @param {number} targetIssueIid - The internal ID of the target project issue
542
- * @param {string} linkType - The type of the relation (relates_to, blocks, is_blocked_by)
543
- * @returns {Promise<GitLabIssueLink>} The created issue link
544
- */
545
- async function createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = "relates_to") {
546
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links`);
547
- const response = await fetch(url.toString(), {
548
- method: "POST",
549
- headers: DEFAULT_HEADERS,
550
- body: JSON.stringify({
551
- target_project_id: targetProjectId,
552
- target_issue_iid: targetIssueIid,
553
- link_type: linkType,
554
- }),
555
- });
556
- await handleGitLabError(response);
557
- const data = await response.json();
558
- return GitLabIssueLinkSchema.parse(data);
559
- }
560
- /**
561
- * Delete an issue link
562
- *
563
- * @param {string} projectId - The ID or URL-encoded path of the project
564
- * @param {number} issueIid - The internal ID of the project issue
565
- * @param {number} issueLinkId - The ID of the issue link
566
- * @returns {Promise<void>}
567
- */
568
- async function deleteIssueLink(projectId, issueIid, issueLinkId) {
569
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}`);
570
- const response = await fetch(url.toString(), {
571
- method: "DELETE",
572
- headers: DEFAULT_HEADERS,
573
- });
574
- await handleGitLabError(response);
575
- }
576
- /**
577
- * Create a new merge request in a GitLab project
578
- *
579
- * @param {string} projectId - The ID or URL-encoded path of the project
580
- * @param {z.infer<typeof CreateMergeRequestOptionsSchema>} options - Merge request creation options
581
- * @returns {Promise<GitLabMergeRequest>} The created merge request
582
- */
583
- async function createMergeRequest(projectId, options) {
584
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`);
585
- const response = await fetch(url.toString(), {
586
- method: "POST",
587
- headers: {
588
- Accept: "application/json",
589
- "Content-Type": "application/json",
590
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
591
- },
592
- body: JSON.stringify({
593
- title: options.title,
594
- description: options.description,
595
- source_branch: options.source_branch,
596
- target_branch: options.target_branch,
597
- allow_collaboration: options.allow_collaboration,
598
- draft: options.draft,
599
- }),
600
- });
601
- if (response.status === 400) {
602
- const errorBody = await response.text();
603
- throw new Error(`Invalid request: ${errorBody}`);
604
- }
605
- if (!response.ok) {
606
- const errorBody = await response.text();
607
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
608
- }
609
- const data = await response.json();
610
- return GitLabMergeRequestSchema.parse(data);
611
- }
612
- /**
613
- * List merge request discussion items
614
- *
615
- * @param {string} projectId - The ID or URL-encoded path of the project
616
- * @param {number} mergeRequestIid - The IID of a merge request
617
- * @returns {Promise<GitLabDiscussion[]>} List of discussions
618
- */
619
- async function listMergeRequestDiscussions(projectId, mergeRequestIid) {
620
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions`);
621
- const response = await fetch(url.toString(), {
622
- headers: DEFAULT_HEADERS,
623
- });
624
- await handleGitLabError(response);
625
- const data = await response.json();
626
- // Ensure the response is parsed as an array of discussions
627
- return z.array(GitLabDiscussionSchema).parse(data);
628
- }
629
- /**
630
- * Modify an existing merge request thread note
631
- *
632
- * @param {string} projectId - The ID or URL-encoded path of the project
633
- * @param {number} mergeRequestIid - The IID of a merge request
634
- * @param {string} discussionId - The ID of a thread
635
- * @param {number} noteId - The ID of a thread note
636
- * @param {string} body - The new content of the note
637
- * @param {boolean} [resolved] - Resolve/unresolve state
638
- * @returns {Promise<GitLabDiscussionNote>} The updated note
639
- */
640
- async function updateMergeRequestNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
641
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
642
- const payload = { body };
643
- if (resolved !== undefined) {
644
- payload.resolved = resolved;
645
- }
646
- const response = await fetch(url.toString(), {
647
- method: "PUT",
648
- headers: DEFAULT_HEADERS,
649
- body: JSON.stringify(payload),
650
- });
651
- await handleGitLabError(response);
652
- const data = await response.json();
653
- return GitLabDiscussionNoteSchema.parse(data);
654
- }
655
- /**
656
- * Create or update a file in a GitLab project
657
- *
658
- * @param {string} projectId - The ID or URL-encoded path of the project
659
- * @param {string} filePath - The path of the file to create or update
660
- * @param {string} content - The content of the file
661
- * @param {string} commitMessage - The commit message
662
- * @param {string} branch - The branch name
663
- * @param {string} [previousPath] - The previous path of the file in case of rename
664
- * @returns {Promise<GitLabCreateUpdateFileResponse>} The file update response
665
- */
666
- async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath, last_commit_id, commit_id) {
667
- const encodedPath = encodeURIComponent(filePath);
668
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
669
- const body = {
670
- branch,
671
- content,
672
- commit_message: commitMessage,
673
- encoding: "text",
674
- ...(previousPath ? { previous_path: previousPath } : {}),
675
- };
676
- // Check if file exists
677
- let method = "POST";
678
- try {
679
- // Get file contents to check existence and retrieve commit IDs
680
- const fileData = await getFileContents(projectId, filePath, branch);
681
- method = "PUT";
682
- // If fileData is not an array, it's a file content object with commit IDs
683
- if (!Array.isArray(fileData)) {
684
- // Use commit IDs from the file data if not provided in parameters
685
- if (!commit_id && fileData.commit_id) {
686
- body.commit_id = fileData.commit_id;
687
- }
688
- else if (commit_id) {
689
- body.commit_id = commit_id;
690
- }
691
- if (!last_commit_id && fileData.last_commit_id) {
692
- body.last_commit_id = fileData.last_commit_id;
693
- }
694
- else if (last_commit_id) {
695
- body.last_commit_id = last_commit_id;
696
- }
697
- }
698
- }
699
- catch (error) {
700
- if (!(error instanceof Error && error.message.includes("File not found"))) {
701
- throw error;
702
- }
703
- // File doesn't exist, use POST - no need for commit IDs for new files
704
- // But still use any provided as parameters if they exist
705
- if (commit_id) {
706
- body.commit_id = commit_id;
707
- }
708
- if (last_commit_id) {
709
- body.last_commit_id = last_commit_id;
710
- }
711
- }
712
- const response = await fetch(url.toString(), {
713
- method,
714
- headers: {
715
- Accept: "application/json",
716
- "Content-Type": "application/json",
717
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
718
- },
719
- body: JSON.stringify(body),
720
- });
721
- if (!response.ok) {
722
- const errorBody = await response.text();
723
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
724
- }
725
- const data = await response.json();
726
- return GitLabCreateUpdateFileResponseSchema.parse(data);
727
- }
728
- /**
729
- * Create a tree structure in a GitLab project repository
730
- *
731
- * @param {string} projectId - The ID or URL-encoded path of the project
732
- * @param {FileOperation[]} files - Array of file operations
733
- * @param {string} [ref] - The name of the branch, tag or commit
734
- * @returns {Promise<GitLabTree>} The created tree
735
- */
736
- async function createTree(projectId, files, ref) {
737
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree`);
738
- if (ref) {
739
- url.searchParams.append("ref", ref);
740
- }
741
- const response = await fetch(url.toString(), {
742
- method: "POST",
743
- headers: {
744
- Accept: "application/json",
745
- "Content-Type": "application/json",
746
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
747
- },
748
- body: JSON.stringify({
749
- files: files.map((file) => ({
750
- file_path: file.path,
751
- content: file.content,
752
- encoding: "text",
753
- })),
754
- }),
755
- });
756
- if (response.status === 400) {
757
- const errorBody = await response.text();
758
- throw new Error(`Invalid request: ${errorBody}`);
759
- }
760
- if (!response.ok) {
761
- const errorBody = await response.text();
762
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
763
- }
764
- const data = await response.json();
765
- return GitLabTreeSchema.parse(data);
766
- }
767
- /**
768
- * Create a commit in a GitLab project repository
769
- *
770
- * @param {string} projectId - The ID or URL-encoded path of the project
771
- * @param {string} message - The commit message
772
- * @param {string} branch - The branch name
773
- * @param {FileOperation[]} actions - Array of file operations for the commit
774
- * @returns {Promise<GitLabCommit>} The created commit
775
- */
776
- async function createCommit(projectId, message, branch, actions) {
777
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`);
778
- const response = await fetch(url.toString(), {
779
- method: "POST",
780
- headers: {
781
- Accept: "application/json",
782
- "Content-Type": "application/json",
783
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
784
- },
785
- body: JSON.stringify({
786
- branch,
787
- commit_message: message,
788
- actions: actions.map((action) => ({
789
- action: "create",
790
- file_path: action.path,
791
- content: action.content,
792
- encoding: "text",
793
- })),
794
- }),
795
- });
796
- if (response.status === 400) {
797
- const errorBody = await response.text();
798
- throw new Error(`Invalid request: ${errorBody}`);
799
- }
800
- if (!response.ok) {
801
- const errorBody = await response.text();
802
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
803
- }
804
- const data = await response.json();
805
- return GitLabCommitSchema.parse(data);
806
- }
807
- /**
808
- * Search for GitLab projects
809
- * 프로젝트 검색
810
- *
811
- * @param {string} query - The search query
812
- * @param {number} [page=1] - The page number
813
- * @param {number} [perPage=20] - Number of items per page
814
- * @returns {Promise<GitLabSearchResponse>} The search results
815
- */
816
- async function searchProjects(query, page = 1, perPage = 20) {
817
- const url = new URL(`${GITLAB_API_URL}/projects`);
818
- url.searchParams.append("search", query);
819
- url.searchParams.append("page", page.toString());
820
- url.searchParams.append("per_page", perPage.toString());
821
- url.searchParams.append("order_by", "id");
822
- url.searchParams.append("sort", "desc");
823
- const response = await fetch(url.toString(), {
824
- headers: {
825
- Accept: "application/json",
826
- "Content-Type": "application/json",
827
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
828
- },
829
- });
830
- if (!response.ok) {
831
- const errorBody = await response.text();
832
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
833
- }
834
- const projects = (await response.json());
835
- const totalCount = response.headers.get("x-total");
836
- const totalPages = response.headers.get("x-total-pages");
837
- // GitLab API doesn't return these headers for results > 10,000
838
- const count = totalCount ? parseInt(totalCount) : projects.length;
839
- return GitLabSearchResponseSchema.parse({
840
- count,
841
- total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
842
- current_page: page,
843
- items: projects,
844
- });
845
- }
846
- /**
847
- * Create a new GitLab repository
848
- * 새 저장소 생성
849
- *
850
- * @param {z.infer<typeof CreateRepositoryOptionsSchema>} options - Repository creation options
851
- * @returns {Promise<GitLabRepository>} The created repository
852
- */
853
- async function createRepository(options) {
854
- const response = await fetch(`${GITLAB_API_URL}/projects`, {
855
- method: "POST",
856
- headers: {
857
- Accept: "application/json",
858
- "Content-Type": "application/json",
859
- Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
860
- },
861
- body: JSON.stringify({
862
- name: options.name,
863
- description: options.description,
864
- visibility: options.visibility,
865
- initialize_with_readme: options.initialize_with_readme,
866
- default_branch: "main",
867
- path: options.name.toLowerCase().replace(/\s+/g, "-"),
868
- }),
869
- });
870
- if (!response.ok) {
871
- const errorBody = await response.text();
872
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
873
- }
874
- const data = await response.json();
875
- return GitLabRepositorySchema.parse(data);
876
- }
877
- /**
878
- * Get merge request details
879
- *
880
- * @param {string} projectId - The ID or URL-encoded path of the project
881
- * @param {number} mergeRequestIid - The internal ID of the merge request
882
- * @returns {Promise<GitLabMergeRequest>} The merge request details
883
- */
884
- async function getMergeRequest(projectId, mergeRequestIid) {
885
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
886
- const response = await fetch(url.toString(), {
887
- headers: DEFAULT_HEADERS,
888
- });
889
- await handleGitLabError(response);
890
- return GitLabMergeRequestSchema.parse(await response.json());
891
- }
892
- /**
893
- * Get merge request changes/diffs
894
- * MR 변경사항 조회 함수 (Function to retrieve merge request changes)
895
- *
896
- * @param {string} projectId - The ID or URL-encoded path of the project
897
- * @param {number} mergeRequestIid - The internal ID of the merge request
898
- * @param {string} [view] - The view type for the diff (inline or parallel)
899
- * @returns {Promise<GitLabMergeRequestDiff[]>} The merge request diffs
900
- */
901
- async function getMergeRequestDiffs(projectId, mergeRequestIid, view) {
902
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/changes`);
903
- if (view) {
904
- url.searchParams.append("view", view);
905
- }
906
- const response = await fetch(url.toString(), {
907
- headers: DEFAULT_HEADERS,
908
- });
909
- await handleGitLabError(response);
910
- const data = (await response.json());
911
- return z.array(GitLabMergeRequestDiffSchema).parse(data.changes);
912
- }
913
- /**
914
- * Update a merge request
915
- *
916
- * @param {string} projectId - The ID or URL-encoded path of the project
917
- * @param {number} mergeRequestIid - The internal ID of the merge request
918
- * @param {Object} options - The update options
919
- * @returns {Promise<GitLabMergeRequest>} The updated merge request
920
- */
921
- async function updateMergeRequest(projectId, mergeRequestIid, options) {
922
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
923
- const response = await fetch(url.toString(), {
924
- method: "PUT",
925
- headers: DEFAULT_HEADERS,
926
- body: JSON.stringify(options),
927
- });
928
- await handleGitLabError(response);
929
- return GitLabMergeRequestSchema.parse(await response.json());
930
- }
931
- /**
932
- * Create a new note (comment) on an issue or merge request
933
- * (New function: createNote - Function to add a note (comment) to an issue or merge request)
934
- *
935
- * @param {string} projectId - The ID or URL-encoded path of the project
936
- * @param {"issue" | "merge_request"} noteableType - The type of the item to add a note to (issue or merge_request)
937
- * @param {number} noteableIid - The internal ID of the issue or merge request
938
- * @param {string} body - The content of the note
939
- * @returns {Promise<any>} The created note
940
- */
941
- async function createNote(projectId, noteableType, noteableIid, body) {
942
- 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
943
- );
944
- const response = await fetch(url.toString(), {
945
- method: "POST",
946
- headers: DEFAULT_HEADERS,
947
- body: JSON.stringify({ body }),
948
- });
949
- if (!response.ok) {
950
- const errorText = await response.text();
951
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
952
- }
953
- return await response.json();
954
- }
955
- /**
956
- * List all namespaces
957
- *
958
- * @param {Object} options - Options for listing namespaces
959
- * @param {string} [options.search] - Search query to filter namespaces
960
- * @param {boolean} [options.owned_only] - Only return namespaces owned by the authenticated user
961
- * @param {boolean} [options.top_level_only] - Only return top-level namespaces
962
- * @returns {Promise<GitLabNamespace[]>} List of namespaces
963
- */
964
- async function listNamespaces(options) {
965
- const url = new URL(`${GITLAB_API_URL}/namespaces`);
966
- if (options.search) {
967
- url.searchParams.append("search", options.search);
968
- }
969
- if (options.owned_only) {
970
- url.searchParams.append("owned_only", "true");
971
- }
972
- if (options.top_level_only) {
973
- url.searchParams.append("top_level_only", "true");
974
- }
975
- const response = await fetch(url.toString(), {
976
- headers: DEFAULT_HEADERS,
977
- });
978
- await handleGitLabError(response);
979
- const data = await response.json();
980
- return z.array(GitLabNamespaceSchema).parse(data);
981
- }
982
- /**
983
- * Get details on a namespace
984
- *
985
- * @param {string} id - The ID or URL-encoded path of the namespace
986
- * @returns {Promise<GitLabNamespace>} The namespace details
987
- */
988
- async function getNamespace(id) {
989
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(id)}`);
990
- const response = await fetch(url.toString(), {
991
- headers: DEFAULT_HEADERS,
992
- });
993
- await handleGitLabError(response);
994
- const data = await response.json();
995
- return GitLabNamespaceSchema.parse(data);
996
- }
997
- /**
998
- * Verify if a namespace exists
999
- *
1000
- * @param {string} namespacePath - The path of the namespace to check
1001
- * @param {number} [parentId] - The ID of the parent namespace
1002
- * @returns {Promise<GitLabNamespaceExistsResponse>} The verification result
1003
- */
1004
- async function verifyNamespaceExistence(namespacePath, parentId) {
1005
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(namespacePath)}/exists`);
1006
- if (parentId) {
1007
- url.searchParams.append("parent_id", parentId.toString());
1008
- }
1009
- const response = await fetch(url.toString(), {
1010
- headers: DEFAULT_HEADERS,
1011
- });
1012
- await handleGitLabError(response);
1013
- const data = await response.json();
1014
- return GitLabNamespaceExistsResponseSchema.parse(data);
1015
- }
1016
- /**
1017
- * Get a single project
1018
- *
1019
- * @param {string} projectId - The ID or URL-encoded path of the project
1020
- * @param {Object} options - Options for getting project details
1021
- * @param {boolean} [options.license] - Include project license data
1022
- * @param {boolean} [options.statistics] - Include project statistics
1023
- * @param {boolean} [options.with_custom_attributes] - Include custom attributes in response
1024
- * @returns {Promise<GitLabProject>} Project details
1025
- */
1026
- async function getProject(projectId, options = {}) {
1027
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`);
1028
- if (options.license) {
1029
- url.searchParams.append("license", "true");
1030
- }
1031
- if (options.statistics) {
1032
- url.searchParams.append("statistics", "true");
1033
- }
1034
- if (options.with_custom_attributes) {
1035
- url.searchParams.append("with_custom_attributes", "true");
1036
- }
1037
- const response = await fetch(url.toString(), {
1038
- headers: DEFAULT_HEADERS,
1039
- });
1040
- await handleGitLabError(response);
1041
- const data = await response.json();
1042
- return GitLabRepositorySchema.parse(data);
1043
- }
1044
- /**
1045
- * List projects
1046
- *
1047
- * @param {Object} options - Options for listing projects
1048
- * @returns {Promise<GitLabProject[]>} List of projects
1049
- */
1050
- async function listProjects(options = {}) {
1051
- // Construct the query parameters
1052
- const params = new URLSearchParams();
1053
- for (const [key, value] of Object.entries(options)) {
1054
- if (value !== undefined && value !== null) {
1055
- if (typeof value === "boolean") {
1056
- params.append(key, value ? "true" : "false");
1057
- }
1058
- else {
1059
- params.append(key, String(value));
1060
- }
1061
- }
1062
- }
1063
- // Make the API request
1064
- const response = await fetch(`${GITLAB_API_URL}/projects?${params.toString()}`, {
1065
- method: "GET",
1066
- headers: DEFAULT_HEADERS,
1067
- });
1068
- // Handle errors
1069
- await handleGitLabError(response);
1070
- // Parse and return the data
1071
- const data = await response.json();
1072
- return z.array(GitLabProjectSchema).parse(data);
1073
- }
1074
- /**
1075
- * List labels for a project
1076
- *
1077
- * @param projectId The ID or URL-encoded path of the project
1078
- * @param options Optional parameters for listing labels
1079
- * @returns Array of GitLab labels
1080
- */
1081
- async function listLabels(projectId, options = {}) {
1082
- // Construct the URL with project path
1083
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`);
1084
- // Add query parameters
1085
- Object.entries(options).forEach(([key, value]) => {
1086
- if (value !== undefined) {
1087
- if (typeof value === "boolean") {
1088
- url.searchParams.append(key, value ? "true" : "false");
1089
- }
1090
- else {
1091
- url.searchParams.append(key, String(value));
1092
- }
1093
- }
1094
- });
1095
- // Make the API request
1096
- const response = await fetch(url.toString(), {
1097
- headers: DEFAULT_HEADERS,
1098
- });
1099
- // Handle errors
1100
- await handleGitLabError(response);
1101
- // Parse and return the data
1102
- const data = await response.json();
1103
- return data;
1104
- }
1105
- /**
1106
- * Get a single label from a project
1107
- *
1108
- * @param projectId The ID or URL-encoded path of the project
1109
- * @param labelId The ID or name of the label
1110
- * @param includeAncestorGroups Whether to include ancestor groups
1111
- * @returns GitLab label
1112
- */
1113
- async function getLabel(projectId, labelId, includeAncestorGroups) {
1114
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`);
1115
- // Add query parameters
1116
- if (includeAncestorGroups !== undefined) {
1117
- url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false");
1118
- }
1119
- // Make the API request
1120
- const response = await fetch(url.toString(), {
1121
- headers: DEFAULT_HEADERS,
1122
- });
1123
- // Handle errors
1124
- await handleGitLabError(response);
1125
- // Parse and return the data
1126
- const data = await response.json();
1127
- return data;
1128
- }
1129
- /**
1130
- * Create a new label in a project
1131
- *
1132
- * @param projectId The ID or URL-encoded path of the project
1133
- * @param options Options for creating the label
1134
- * @returns Created GitLab label
1135
- */
1136
- async function createLabel(projectId, options) {
1137
- // Make the API request
1138
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`, {
1139
- method: "POST",
1140
- headers: DEFAULT_HEADERS,
1141
- body: JSON.stringify(options),
1142
- });
1143
- // Handle errors
1144
- await handleGitLabError(response);
1145
- // Parse and return the data
1146
- const data = await response.json();
1147
- return data;
1148
- }
1149
- /**
1150
- * Update an existing label in a project
1151
- *
1152
- * @param projectId The ID or URL-encoded path of the project
1153
- * @param labelId The ID or name of the label to update
1154
- * @param options Options for updating the label
1155
- * @returns Updated GitLab label
1156
- */
1157
- async function updateLabel(projectId, labelId, options) {
1158
- // Make the API request
1159
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, {
1160
- method: "PUT",
1161
- headers: DEFAULT_HEADERS,
1162
- body: JSON.stringify(options),
1163
- });
1164
- // Handle errors
1165
- await handleGitLabError(response);
1166
- // Parse and return the data
1167
- const data = await response.json();
1168
- return data;
1169
- }
1170
- /**
1171
- * Delete a label from a project
1172
- *
1173
- * @param projectId The ID or URL-encoded path of the project
1174
- * @param labelId The ID or name of the label to delete
1175
- */
1176
- async function deleteLabel(projectId, labelId) {
1177
- // Make the API request
1178
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, {
1179
- method: "DELETE",
1180
- headers: DEFAULT_HEADERS,
1181
- });
1182
- // Handle errors
1183
- await handleGitLabError(response);
1184
- }
1185
- /**
1186
- * List all projects in a GitLab group
1187
- *
1188
- * @param {z.infer<typeof ListGroupProjectsSchema>} options - Options for listing group projects
1189
- * @returns {Promise<GitLabProject[]>} Array of projects in the group
1190
- */
1191
- async function listGroupProjects(options) {
1192
- const url = new URL(`${GITLAB_API_URL}/groups/${encodeURIComponent(options.group_id)}/projects`);
1193
- // Add optional parameters to URL
1194
- if (options.include_subgroups)
1195
- url.searchParams.append("include_subgroups", "true");
1196
- if (options.search)
1197
- url.searchParams.append("search", options.search);
1198
- if (options.order_by)
1199
- url.searchParams.append("order_by", options.order_by);
1200
- if (options.sort)
1201
- url.searchParams.append("sort", options.sort);
1202
- if (options.page)
1203
- url.searchParams.append("page", options.page.toString());
1204
- if (options.per_page)
1205
- url.searchParams.append("per_page", options.per_page.toString());
1206
- if (options.archived !== undefined)
1207
- url.searchParams.append("archived", options.archived.toString());
1208
- if (options.visibility)
1209
- url.searchParams.append("visibility", options.visibility);
1210
- if (options.with_issues_enabled !== undefined)
1211
- url.searchParams.append("with_issues_enabled", options.with_issues_enabled.toString());
1212
- if (options.with_merge_requests_enabled !== undefined)
1213
- url.searchParams.append("with_merge_requests_enabled", options.with_merge_requests_enabled.toString());
1214
- if (options.min_access_level !== undefined)
1215
- url.searchParams.append("min_access_level", options.min_access_level.toString());
1216
- if (options.with_programming_language)
1217
- url.searchParams.append("with_programming_language", options.with_programming_language);
1218
- if (options.starred !== undefined)
1219
- url.searchParams.append("starred", options.starred.toString());
1220
- if (options.statistics !== undefined)
1221
- url.searchParams.append("statistics", options.statistics.toString());
1222
- if (options.with_custom_attributes !== undefined)
1223
- url.searchParams.append("with_custom_attributes", options.with_custom_attributes.toString());
1224
- if (options.with_security_reports !== undefined)
1225
- url.searchParams.append("with_security_reports", options.with_security_reports.toString());
1226
- const response = await fetch(url.toString(), {
1227
- method: "GET",
1228
- headers: DEFAULT_HEADERS,
1229
- });
1230
- await handleGitLabError(response);
1231
- const projects = await response.json();
1232
- return GitLabProjectSchema.array().parse(projects);
1233
- }
1234
- server.setRequestHandler(ListToolsRequestSchema, async () => {
1235
- // If read-only mode is enabled, filter out write operations
1236
- const tools = GITLAB_READ_ONLY_MODE
1237
- ? allTools.filter((tool) => readOnlyTools.includes(tool.name))
1238
- : allTools;
1239
- return {
1240
- tools,
1241
- };
1242
- });
1243
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1244
- try {
1245
- if (!request.params.arguments) {
1246
- throw new Error("Arguments are required");
1247
- }
1248
- switch (request.params.name) {
1249
- case "fork_repository": {
1250
- const forkArgs = ForkRepositorySchema.parse(request.params.arguments);
1251
- try {
1252
- const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace);
1253
- return {
1254
- content: [
1255
- { type: "text", text: JSON.stringify(forkedProject, null, 2) },
1256
- ],
1257
- };
1258
- }
1259
- catch (forkError) {
1260
- console.error("Error forking repository:", forkError);
1261
- let forkErrorMessage = "Failed to fork repository";
1262
- if (forkError instanceof Error) {
1263
- forkErrorMessage = `${forkErrorMessage}: ${forkError.message}`;
1264
- }
1265
- return {
1266
- content: [
1267
- {
1268
- type: "text",
1269
- text: JSON.stringify({ error: forkErrorMessage }, null, 2),
1270
- },
1271
- ],
1272
- };
1273
- }
1274
- }
1275
- case "create_branch": {
1276
- const args = CreateBranchSchema.parse(request.params.arguments);
1277
- let ref = args.ref;
1278
- if (!ref) {
1279
- ref = await getDefaultBranchRef(args.project_id);
1280
- }
1281
- const branch = await createBranch(args.project_id, {
1282
- name: args.branch,
1283
- ref,
1284
- });
1285
- return {
1286
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
1287
- };
1288
- }
1289
- case "search_repositories": {
1290
- const args = SearchRepositoriesSchema.parse(request.params.arguments);
1291
- const results = await searchProjects(args.search, args.page, args.per_page);
1292
- return {
1293
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
1294
- };
1295
- }
1296
- case "create_repository": {
1297
- const args = CreateRepositorySchema.parse(request.params.arguments);
1298
- const repository = await createRepository(args);
1299
- return {
1300
- content: [
1301
- { type: "text", text: JSON.stringify(repository, null, 2) },
1302
- ],
1303
- };
1304
- }
1305
- case "get_file_contents": {
1306
- const args = GetFileContentsSchema.parse(request.params.arguments);
1307
- const contents = await getFileContents(args.project_id, args.file_path, args.ref);
1308
- return {
1309
- content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
1310
- };
1311
- }
1312
- case "create_or_update_file": {
1313
- const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
1314
- 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);
1315
- return {
1316
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1317
- };
1318
- }
1319
- case "push_files": {
1320
- const args = PushFilesSchema.parse(request.params.arguments);
1321
- const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map((f) => ({ path: f.file_path, content: f.content })));
1322
- return {
1323
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1324
- };
1325
- }
1326
- case "create_issue": {
1327
- const args = CreateIssueSchema.parse(request.params.arguments);
1328
- const { project_id, ...options } = args;
1329
- const issue = await createIssue(project_id, options);
1330
- return {
1331
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
1332
- };
1333
- }
1334
- case "create_merge_request": {
1335
- const args = CreateMergeRequestSchema.parse(request.params.arguments);
1336
- const { project_id, ...options } = args;
1337
- const mergeRequest = await createMergeRequest(project_id, options);
1338
- return {
1339
- content: [
1340
- { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
1341
- ],
1342
- };
1343
- }
1344
- case "update_merge_request_note": {
1345
- const args = UpdateMergeRequestNoteSchema.parse(request.params.arguments);
1346
- 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
1347
- );
1348
- return {
1349
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
1350
- };
1351
- }
1352
- case "get_merge_request": {
1353
- const args = GetMergeRequestSchema.parse(request.params.arguments);
1354
- const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid);
1355
- return {
1356
- content: [
1357
- { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
1358
- ],
1359
- };
1360
- }
1361
- case "get_merge_request_diffs": {
1362
- const args = GetMergeRequestDiffsSchema.parse(request.params.arguments);
1363
- const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.view);
1364
- return {
1365
- content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
1366
- };
1367
- }
1368
- case "update_merge_request": {
1369
- const args = UpdateMergeRequestSchema.parse(request.params.arguments);
1370
- const { project_id, merge_request_iid, ...options } = args;
1371
- const mergeRequest = await updateMergeRequest(project_id, merge_request_iid, options);
1372
- return {
1373
- content: [
1374
- { type: "text", text: JSON.stringify(mergeRequest, null, 2) },
1375
- ],
1376
- };
1377
- }
1378
- case "list_merge_request_discussions": {
1379
- const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments);
1380
- const discussions = await listMergeRequestDiscussions(args.project_id, args.merge_request_iid);
1381
- return {
1382
- content: [
1383
- { type: "text", text: JSON.stringify(discussions, null, 2) },
1384
- ],
1385
- };
1386
- }
1387
- case "list_namespaces": {
1388
- const args = ListNamespacesSchema.parse(request.params.arguments);
1389
- const url = new URL(`${GITLAB_API_URL}/namespaces`);
1390
- if (args.search) {
1391
- url.searchParams.append("search", args.search);
1392
- }
1393
- if (args.page) {
1394
- url.searchParams.append("page", args.page.toString());
1395
- }
1396
- if (args.per_page) {
1397
- url.searchParams.append("per_page", args.per_page.toString());
1398
- }
1399
- if (args.owned) {
1400
- url.searchParams.append("owned", args.owned.toString());
1401
- }
1402
- const response = await fetch(url.toString(), {
1403
- headers: DEFAULT_HEADERS,
1404
- });
1405
- await handleGitLabError(response);
1406
- const data = await response.json();
1407
- const namespaces = z.array(GitLabNamespaceSchema).parse(data);
1408
- return {
1409
- content: [
1410
- { type: "text", text: JSON.stringify(namespaces, null, 2) },
1411
- ],
1412
- };
1413
- }
1414
- case "get_namespace": {
1415
- const args = GetNamespaceSchema.parse(request.params.arguments);
1416
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.namespace_id)}`);
1417
- const response = await fetch(url.toString(), {
1418
- headers: DEFAULT_HEADERS,
1419
- });
1420
- await handleGitLabError(response);
1421
- const data = await response.json();
1422
- const namespace = GitLabNamespaceSchema.parse(data);
1423
- return {
1424
- content: [{ type: "text", text: JSON.stringify(namespace, null, 2) }],
1425
- };
1426
- }
1427
- case "verify_namespace": {
1428
- const args = VerifyNamespaceSchema.parse(request.params.arguments);
1429
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists`);
1430
- const response = await fetch(url.toString(), {
1431
- headers: DEFAULT_HEADERS,
1432
- });
1433
- await handleGitLabError(response);
1434
- const data = await response.json();
1435
- const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data);
1436
- return {
1437
- content: [
1438
- { type: "text", text: JSON.stringify(namespaceExists, null, 2) },
1439
- ],
1440
- };
1441
- }
1442
- case "get_project": {
1443
- const args = GetProjectSchema.parse(request.params.arguments);
1444
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(args.project_id)}`);
1445
- const response = await fetch(url.toString(), {
1446
- headers: DEFAULT_HEADERS,
1447
- });
1448
- await handleGitLabError(response);
1449
- const data = await response.json();
1450
- const project = GitLabProjectSchema.parse(data);
1451
- return {
1452
- content: [{ type: "text", text: JSON.stringify(project, null, 2) }],
1453
- };
1454
- }
1455
- case "list_projects": {
1456
- const args = ListProjectsSchema.parse(request.params.arguments);
1457
- const projects = await listProjects(args);
1458
- return {
1459
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
1460
- };
1461
- }
1462
- case "create_note": {
1463
- const args = CreateNoteSchema.parse(request.params.arguments);
1464
- const { project_id, noteable_type, noteable_iid, body } = args;
1465
- const note = await createNote(project_id, noteable_type, noteable_iid, body);
1466
- return {
1467
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
1468
- };
1469
- }
1470
- case "list_issues": {
1471
- const args = ListIssuesSchema.parse(request.params.arguments);
1472
- const { project_id, ...options } = args;
1473
- const issues = await listIssues(project_id, options);
1474
- return {
1475
- content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
1476
- };
1477
- }
1478
- case "get_issue": {
1479
- const args = GetIssueSchema.parse(request.params.arguments);
1480
- const issue = await getIssue(args.project_id, args.issue_iid);
1481
- return {
1482
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
1483
- };
1484
- }
1485
- case "update_issue": {
1486
- const args = UpdateIssueSchema.parse(request.params.arguments);
1487
- const { project_id, issue_iid, ...options } = args;
1488
- const issue = await updateIssue(project_id, issue_iid, options);
1489
- return {
1490
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
1491
- };
1492
- }
1493
- case "delete_issue": {
1494
- const args = DeleteIssueSchema.parse(request.params.arguments);
1495
- await deleteIssue(args.project_id, args.issue_iid);
1496
- return {
1497
- content: [
1498
- {
1499
- type: "text",
1500
- text: JSON.stringify({ status: "success", message: "Issue deleted successfully" }, null, 2),
1501
- },
1502
- ],
1503
- };
1504
- }
1505
- case "list_issue_links": {
1506
- const args = ListIssueLinksSchema.parse(request.params.arguments);
1507
- const links = await listIssueLinks(args.project_id, args.issue_iid);
1508
- return {
1509
- content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
1510
- };
1511
- }
1512
- case "get_issue_link": {
1513
- const args = GetIssueLinkSchema.parse(request.params.arguments);
1514
- const link = await getIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
1515
- return {
1516
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
1517
- };
1518
- }
1519
- case "create_issue_link": {
1520
- const args = CreateIssueLinkSchema.parse(request.params.arguments);
1521
- const link = await createIssueLink(args.project_id, args.issue_iid, args.target_project_id, args.target_issue_iid, args.link_type);
1522
- return {
1523
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
1524
- };
1525
- }
1526
- case "delete_issue_link": {
1527
- const args = DeleteIssueLinkSchema.parse(request.params.arguments);
1528
- await deleteIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
1529
- return {
1530
- content: [
1531
- {
1532
- type: "text",
1533
- text: JSON.stringify({
1534
- status: "success",
1535
- message: "Issue link deleted successfully",
1536
- }, null, 2),
1537
- },
1538
- ],
1539
- };
1540
- }
1541
- case "list_labels": {
1542
- const args = ListLabelsSchema.parse(request.params.arguments);
1543
- const labels = await listLabels(args.project_id, args);
1544
- return {
1545
- content: [{ type: "text", text: JSON.stringify(labels, null, 2) }],
1546
- };
1547
- }
1548
- case "get_label": {
1549
- const args = GetLabelSchema.parse(request.params.arguments);
1550
- const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups);
1551
- return {
1552
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
1553
- };
1554
- }
1555
- case "create_label": {
1556
- const args = CreateLabelSchema.parse(request.params.arguments);
1557
- const label = await createLabel(args.project_id, args);
1558
- return {
1559
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
1560
- };
1561
- }
1562
- case "update_label": {
1563
- const args = UpdateLabelSchema.parse(request.params.arguments);
1564
- const { project_id, label_id, ...options } = args;
1565
- const label = await updateLabel(project_id, label_id, options);
1566
- return {
1567
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
1568
- };
1569
- }
1570
- case "delete_label": {
1571
- const args = DeleteLabelSchema.parse(request.params.arguments);
1572
- await deleteLabel(args.project_id, args.label_id);
1573
- return {
1574
- content: [
1575
- {
1576
- type: "text",
1577
- text: JSON.stringify({ status: "success", message: "Label deleted successfully" }, null, 2),
1578
- },
1579
- ],
1580
- };
1581
- }
1582
- case "list_group_projects": {
1583
- const args = ListGroupProjectsSchema.parse(request.params.arguments);
1584
- const projects = await listGroupProjects(args);
1585
- return {
1586
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
1587
- };
1588
- }
1589
- default:
1590
- throw new Error(`Unknown tool: ${request.params.name}`);
1591
- }
1592
- }
1593
- catch (error) {
1594
- if (error instanceof z.ZodError) {
1595
- throw new Error(`Invalid arguments: ${error.errors
1596
- .map((e) => `${e.path.join(".")}: ${e.message}`)
1597
- .join(", ")}`);
1598
- }
1599
- throw error;
1600
- }
1601
- });
1602
- /**
1603
- * Initialize and run the server
1604
- */
1605
- async function runServer() {
1606
- if (isSSE) {
1607
- const app = express();
1608
- let transport;
1609
- app.get("/sse", async (req, res) => {
1610
- console.log("Establishing new SSE connection");
1611
- transport = new SSEServerTransport("/messages", res);
1612
- await server.connect(transport);
1613
- });
1614
- app.post("/messages", async (req, res) => {
1615
- await transport?.handlePostMessage(req, res);
1616
- });
1617
- app.listen(port, () => {
1618
- console.log(`HTTP server listening on port ${port}`);
1619
- console.log(`SSE endpoint available at http://localhost:${port}/sse`);
1620
- console.log(`Message endpoint available at http://localhost:${port}/messages`);
1621
- });
1622
- }
1623
- else {
1624
- try {
1625
- console.error("========================");
1626
- console.error(`GitLab MCP Server v${SERVER_VERSION}`);
1627
- console.error(`API URL: ${GITLAB_API_URL}`);
1628
- console.error("========================");
1629
- const transport = new StdioServerTransport();
1630
- await server.connect(transport);
1631
- console.error("GitLab MCP Server running on stdio");
1632
- }
1633
- catch (error) {
1634
- console.error("Error initializing server:", error);
1635
- process.exit(1);
1636
- }
1637
- }
1638
- }
1639
- runServer().catch((error) => {
1640
- console.error("Fatal error in main():", error);
1641
- process.exit(1);
1642
- });