@zereight/mcp-gitlab 1.0.77 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,4117 +1,250 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
- import express from "express";
8
- import fetchCookie from "fetch-cookie";
9
- import fs from "fs";
10
- import { HttpProxyAgent } from "http-proxy-agent";
11
- import { HttpsProxyAgent } from "https-proxy-agent";
12
- import nodeFetch from "node-fetch";
13
- import path, { dirname } from "path";
14
- import { SocksProxyAgent } from "socks-proxy-agent";
15
- import { CookieJar, parse as parseCookie } from "tough-cookie";
16
- import { fileURLToPath } from "url";
17
- import { z } from "zod";
18
- import { zodToJsonSchema } from "zod-to-json-schema";
19
- // Add type imports for proxy agents
20
- import { Agent } from "http";
21
- import { Agent as HttpsAgent } from "https";
22
- import { URL } from "url";
23
- import { BulkPublishDraftNotesSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateLabelSchema, // Added
24
- CreateMergeRequestNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, DeleteDraftNoteSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema,
25
- // pipeline job schemas
26
- GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
27
- // Discussion Schemas
28
- GitLabDiscussionNoteSchema, // Added
29
- GitLabDiscussionSchema,
30
- // Draft Notes Schemas
31
- GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabTreeSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
32
- ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, MergeMergeRequestSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema } from "./schemas.js";
33
- import { randomUUID } from "crypto";
34
- import { pino } from "pino";
35
- const logger = pino({
36
- level: process.env.LOG_LEVEL || "info",
37
- transport: {
38
- target: "pino-pretty",
39
- options: {
40
- colorize: true,
41
- levelFirst: true,
42
- destination: 2,
43
- },
44
- },
45
- });
46
- /**
47
- * Available transport modes for MCP server
48
- */
49
- var TransportMode;
50
- (function (TransportMode) {
51
- TransportMode["STDIO"] = "stdio";
52
- TransportMode["SSE"] = "sse";
53
- TransportMode["STREAMABLE_HTTP"] = "streamable-http";
54
- })(TransportMode || (TransportMode = {}));
55
- /**
56
- * Read version from package.json
57
- */
58
- const __filename = fileURLToPath(import.meta.url);
59
- const __dirname = dirname(__filename);
60
- const packageJsonPath = path.resolve(__dirname, "../package.json");
61
- let SERVER_VERSION = "unknown";
62
- try {
63
- if (fs.existsSync(packageJsonPath)) {
64
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
65
- SERVER_VERSION = packageJson.version || SERVER_VERSION;
66
- }
67
- }
68
- catch (error) {
69
- // Warning: Could not read version from package.json - silently continue
70
- }
71
- const server = new Server({
72
- name: "better-gitlab-mcp-server",
73
- version: SERVER_VERSION,
74
- }, {
75
- capabilities: {
76
- tools: {},
77
- },
78
- });
79
- const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
80
- const GITLAB_AUTH_COOKIE_PATH = process.env.GITLAB_AUTH_COOKIE_PATH;
81
- const IS_OLD = process.env.GITLAB_IS_OLD === "true";
82
- const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
83
- const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true";
84
- const USE_MILESTONE = process.env.USE_MILESTONE === "true";
85
- const USE_PIPELINE = process.env.USE_PIPELINE === "true";
86
- const SSE = process.env.SSE === "true";
87
- const STREAMABLE_HTTP = process.env.STREAMABLE_HTTP === "true";
88
- const HOST = process.env.HOST || "0.0.0.0";
89
- const PORT = process.env.PORT || 3002;
90
- // Add proxy configuration
91
- const HTTP_PROXY = process.env.HTTP_PROXY;
92
- const HTTPS_PROXY = process.env.HTTPS_PROXY;
93
- const NODE_TLS_REJECT_UNAUTHORIZED = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
94
- const GITLAB_CA_CERT_PATH = process.env.GITLAB_CA_CERT_PATH;
95
- let sslOptions = undefined;
96
- if (NODE_TLS_REJECT_UNAUTHORIZED === "0") {
97
- sslOptions = { rejectUnauthorized: false };
98
- }
99
- else if (GITLAB_CA_CERT_PATH) {
100
- const ca = fs.readFileSync(GITLAB_CA_CERT_PATH);
101
- sslOptions = { ca };
102
- }
103
- // Configure proxy agents if proxies are set
104
- let httpAgent = undefined;
105
- let httpsAgent = undefined;
106
- if (HTTP_PROXY) {
107
- if (HTTP_PROXY.startsWith("socks")) {
108
- httpAgent = new SocksProxyAgent(HTTP_PROXY);
109
- }
110
- else {
111
- httpAgent = new HttpProxyAgent(HTTP_PROXY);
112
- }
113
- }
114
- if (HTTPS_PROXY) {
115
- if (HTTPS_PROXY.startsWith("socks")) {
116
- httpsAgent = new SocksProxyAgent(HTTPS_PROXY);
117
- }
118
- else {
119
- httpsAgent = new HttpsProxyAgent(HTTPS_PROXY, sslOptions);
120
- }
121
- }
122
- httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
123
- httpAgent = httpAgent || new Agent();
124
- // Create cookie jar with clean Netscape file parsing
125
- const createCookieJar = () => {
126
- if (!GITLAB_AUTH_COOKIE_PATH)
127
- return null;
128
- try {
129
- const cookiePath = GITLAB_AUTH_COOKIE_PATH.startsWith("~/")
130
- ? path.join(process.env.HOME || "", GITLAB_AUTH_COOKIE_PATH.slice(2))
131
- : GITLAB_AUTH_COOKIE_PATH;
132
- const jar = new CookieJar();
133
- const cookieContent = fs.readFileSync(cookiePath, "utf8");
134
- cookieContent.split("\n").forEach(line => {
135
- // Handle #HttpOnly_ prefix
136
- if (line.startsWith("#HttpOnly_")) {
137
- line = line.slice(10);
138
- }
139
- // Skip comments and empty lines
140
- if (line.startsWith("#") || !line.trim()) {
141
- return;
142
- }
143
- // Parse Netscape format: domain, flag, path, secure, expires, name, value
144
- const parts = line.split("\t");
145
- if (parts.length >= 7) {
146
- const [domain, , path, secure, expires, name, value] = parts;
147
- // Build cookie string in standard format
148
- const cookieStr = `${name}=${value}; Domain=${domain}; Path=${path}${secure === "TRUE" ? "; Secure" : ""}${expires !== "0" ? `; Expires=${new Date(parseInt(expires) * 1000).toUTCString()}` : ""}`;
149
- // Use tough-cookie's parse function for robust parsing
150
- const cookie = parseCookie(cookieStr);
151
- if (cookie) {
152
- const url = `${secure === "TRUE" ? "https" : "http"}://${domain.startsWith(".") ? domain.slice(1) : domain}`;
153
- jar.setCookieSync(cookie, url);
154
- }
155
- }
156
- });
157
- return jar;
158
- }
159
- catch (error) {
160
- logger.error("Error loading cookie file:", error);
161
- return null;
162
- }
163
- };
164
- // Initialize cookie jar and fetch
165
- const cookieJar = createCookieJar();
166
- const fetch = cookieJar ? fetchCookie(nodeFetch, cookieJar) : nodeFetch;
167
- // Ensure session is established for the current request
168
- async function ensureSessionForRequest() {
169
- if (!cookieJar || !GITLAB_AUTH_COOKIE_PATH)
170
- return;
171
- // Extract the base URL from GITLAB_API_URL
172
- const apiUrl = new URL(GITLAB_API_URL);
173
- const baseUrl = `${apiUrl.protocol}//${apiUrl.hostname}`;
174
- // Check if we already have GitLab session cookies
175
- const gitlabCookies = cookieJar.getCookiesSync(baseUrl);
176
- const hasSessionCookie = gitlabCookies.some(cookie => cookie.key === "_gitlab_session" || cookie.key === "remember_user_token");
177
- if (!hasSessionCookie) {
178
- try {
179
- // Establish session with a lightweight request
180
- await fetch(`${GITLAB_API_URL}/user`, {
181
- ...DEFAULT_FETCH_CONFIG,
182
- redirect: "follow",
183
- }).catch(() => {
184
- // Ignore errors - the important thing is that cookies get set during redirects
185
- });
186
- // Small delay to ensure cookies are fully processed
187
- await new Promise(resolve => setTimeout(resolve, 100));
188
- }
189
- catch (error) {
190
- // Ignore session establishment errors
191
- }
192
- }
193
- }
194
- // Modify DEFAULT_HEADERS to include agent configuration
195
- const DEFAULT_HEADERS = {
196
- Accept: "application/json",
197
- "Content-Type": "application/json",
198
- };
199
- if (IS_OLD) {
200
- DEFAULT_HEADERS["Private-Token"] = `${GITLAB_PERSONAL_ACCESS_TOKEN}`;
201
- }
202
- else {
203
- DEFAULT_HEADERS["Authorization"] = `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`;
204
- }
205
- // Create a default fetch configuration object that includes proxy agents if set
206
- const DEFAULT_FETCH_CONFIG = {
207
- headers: DEFAULT_HEADERS,
208
- agent: (parsedUrl) => {
209
- if (parsedUrl.protocol === "https:") {
210
- return httpsAgent;
211
- }
212
- return httpAgent;
213
- },
214
- };
215
- // Define all available tools
216
- const allTools = [
217
- {
218
- name: "merge_merge_request",
219
- description: "Merge a merge request in a GitLab project",
220
- inputSchema: zodToJsonSchema(MergeMergeRequestSchema),
221
- },
222
- {
223
- name: "create_or_update_file",
224
- description: "Create or update a single file in a GitLab project",
225
- inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
226
- },
227
- {
228
- name: "search_repositories",
229
- description: "Search for GitLab projects",
230
- inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
231
- },
232
- {
233
- name: "create_repository",
234
- description: "Create a new GitLab project",
235
- inputSchema: zodToJsonSchema(CreateRepositorySchema),
236
- },
237
- {
238
- name: "get_file_contents",
239
- description: "Get the contents of a file or directory from a GitLab project",
240
- inputSchema: zodToJsonSchema(GetFileContentsSchema),
241
- },
242
- {
243
- name: "push_files",
244
- description: "Push multiple files to a GitLab project in a single commit",
245
- inputSchema: zodToJsonSchema(PushFilesSchema),
246
- },
247
- {
248
- name: "create_issue",
249
- description: "Create a new issue in a GitLab project",
250
- inputSchema: zodToJsonSchema(CreateIssueSchema),
251
- },
252
- {
253
- name: "create_merge_request",
254
- description: "Create a new merge request in a GitLab project",
255
- inputSchema: zodToJsonSchema(CreateMergeRequestSchema),
256
- },
257
- {
258
- name: "fork_repository",
259
- description: "Fork a GitLab project to your account or specified namespace",
260
- inputSchema: zodToJsonSchema(ForkRepositorySchema),
261
- },
262
- {
263
- name: "create_branch",
264
- description: "Create a new branch in a GitLab project",
265
- inputSchema: zodToJsonSchema(CreateBranchSchema),
266
- },
267
- {
268
- name: "get_merge_request",
269
- description: "Get details of a merge request (Either mergeRequestIid or branchName must be provided)",
270
- inputSchema: zodToJsonSchema(GetMergeRequestSchema),
271
- },
272
- {
273
- name: "get_merge_request_diffs",
274
- description: "Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)",
275
- inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
276
- },
277
- {
278
- name: "list_merge_request_diffs",
279
- description: "List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)",
280
- inputSchema: zodToJsonSchema(ListMergeRequestDiffsSchema),
281
- },
282
- {
283
- name: "get_branch_diffs",
284
- description: "Get the changes/diffs between two branches or commits in a GitLab project",
285
- inputSchema: zodToJsonSchema(GetBranchDiffsSchema),
286
- },
287
- {
288
- name: "update_merge_request",
289
- description: "Update a merge request (Either mergeRequestIid or branchName must be provided)",
290
- inputSchema: zodToJsonSchema(UpdateMergeRequestSchema),
291
- },
292
- {
293
- name: "create_note",
294
- description: "Create a new note (comment) to an issue or merge request",
295
- inputSchema: zodToJsonSchema(CreateNoteSchema),
296
- },
297
- {
298
- name: "create_merge_request_thread",
299
- description: "Create a new thread on a merge request",
300
- inputSchema: zodToJsonSchema(CreateMergeRequestThreadSchema),
301
- },
302
- {
303
- name: "mr_discussions",
304
- description: "List discussion items for a merge request",
305
- inputSchema: zodToJsonSchema(ListMergeRequestDiscussionsSchema),
306
- },
307
- {
308
- name: "update_merge_request_note",
309
- description: "Modify an existing merge request thread note",
310
- inputSchema: zodToJsonSchema(UpdateMergeRequestNoteSchema),
311
- },
312
- {
313
- name: "create_merge_request_note",
314
- description: "Add a new note to an existing merge request thread",
315
- inputSchema: zodToJsonSchema(CreateMergeRequestNoteSchema),
316
- },
317
- {
318
- name: "get_draft_note",
319
- description: "Get a single draft note from a merge request",
320
- inputSchema: zodToJsonSchema(GetDraftNoteSchema),
321
- },
322
- {
323
- name: "list_draft_notes",
324
- description: "List draft notes for a merge request",
325
- inputSchema: zodToJsonSchema(ListDraftNotesSchema),
326
- },
327
- {
328
- name: "create_draft_note",
329
- description: "Create a draft note for a merge request",
330
- inputSchema: zodToJsonSchema(CreateDraftNoteSchema),
331
- },
332
- {
333
- name: "update_draft_note",
334
- description: "Update an existing draft note",
335
- inputSchema: zodToJsonSchema(UpdateDraftNoteSchema),
336
- },
337
- {
338
- name: "delete_draft_note",
339
- description: "Delete a draft note",
340
- inputSchema: zodToJsonSchema(DeleteDraftNoteSchema),
341
- },
342
- {
343
- name: "publish_draft_note",
344
- description: "Publish a single draft note",
345
- inputSchema: zodToJsonSchema(PublishDraftNoteSchema),
346
- },
347
- {
348
- name: "bulk_publish_draft_notes",
349
- description: "Publish all draft notes for a merge request",
350
- inputSchema: zodToJsonSchema(BulkPublishDraftNotesSchema),
351
- },
352
- {
353
- name: "update_issue_note",
354
- description: "Modify an existing issue thread note",
355
- inputSchema: zodToJsonSchema(UpdateIssueNoteSchema),
356
- },
357
- {
358
- name: "create_issue_note",
359
- description: "Add a new note to an existing issue thread",
360
- inputSchema: zodToJsonSchema(CreateIssueNoteSchema),
361
- },
362
- {
363
- name: "list_issues",
364
- description: "List issues (default: created by current user only; use scope='all' for all accessible issues)",
365
- inputSchema: zodToJsonSchema(ListIssuesSchema),
366
- },
367
- {
368
- name: "my_issues",
369
- description: "List issues assigned to the authenticated user (defaults to open issues)",
370
- inputSchema: zodToJsonSchema(MyIssuesSchema),
371
- },
372
- {
373
- name: "get_issue",
374
- description: "Get details of a specific issue in a GitLab project",
375
- inputSchema: zodToJsonSchema(GetIssueSchema),
376
- },
377
- {
378
- name: "update_issue",
379
- description: "Update an issue in a GitLab project",
380
- inputSchema: zodToJsonSchema(UpdateIssueSchema),
381
- },
382
- {
383
- name: "delete_issue",
384
- description: "Delete an issue from a GitLab project",
385
- inputSchema: zodToJsonSchema(DeleteIssueSchema),
386
- },
387
- {
388
- name: "list_issue_links",
389
- description: "List all issue links for a specific issue",
390
- inputSchema: zodToJsonSchema(ListIssueLinksSchema),
391
- },
392
- {
393
- name: "list_issue_discussions",
394
- description: "List discussions for an issue in a GitLab project",
395
- inputSchema: zodToJsonSchema(ListIssueDiscussionsSchema),
396
- },
397
- {
398
- name: "get_issue_link",
399
- description: "Get a specific issue link",
400
- inputSchema: zodToJsonSchema(GetIssueLinkSchema),
401
- },
402
- {
403
- name: "create_issue_link",
404
- description: "Create an issue link between two issues",
405
- inputSchema: zodToJsonSchema(CreateIssueLinkSchema),
406
- },
407
- {
408
- name: "delete_issue_link",
409
- description: "Delete an issue link",
410
- inputSchema: zodToJsonSchema(DeleteIssueLinkSchema),
411
- },
412
- {
413
- name: "list_namespaces",
414
- description: "List all namespaces available to the current user",
415
- inputSchema: zodToJsonSchema(ListNamespacesSchema),
416
- },
417
- {
418
- name: "get_namespace",
419
- description: "Get details of a namespace by ID or path",
420
- inputSchema: zodToJsonSchema(GetNamespaceSchema),
421
- },
422
- {
423
- name: "verify_namespace",
424
- description: "Verify if a namespace path exists",
425
- inputSchema: zodToJsonSchema(VerifyNamespaceSchema),
426
- },
427
- {
428
- name: "get_project",
429
- description: "Get details of a specific project",
430
- inputSchema: zodToJsonSchema(GetProjectSchema),
431
- },
432
- {
433
- name: "list_projects",
434
- description: "List projects accessible by the current user",
435
- inputSchema: zodToJsonSchema(ListProjectsSchema),
436
- },
437
- {
438
- name: "list_project_members",
439
- description: "List members of a GitLab project",
440
- inputSchema: zodToJsonSchema(ListProjectMembersSchema),
441
- },
442
- {
443
- name: "list_labels",
444
- description: "List labels for a project",
445
- inputSchema: zodToJsonSchema(ListLabelsSchema),
446
- },
447
- {
448
- name: "get_label",
449
- description: "Get a single label from a project",
450
- inputSchema: zodToJsonSchema(GetLabelSchema),
451
- },
452
- {
453
- name: "create_label",
454
- description: "Create a new label in a project",
455
- inputSchema: zodToJsonSchema(CreateLabelSchema),
456
- },
457
- {
458
- name: "update_label",
459
- description: "Update an existing label in a project",
460
- inputSchema: zodToJsonSchema(UpdateLabelSchema),
461
- },
462
- {
463
- name: "delete_label",
464
- description: "Delete a label from a project",
465
- inputSchema: zodToJsonSchema(DeleteLabelSchema),
466
- },
467
- {
468
- name: "list_group_projects",
469
- description: "List projects in a GitLab group with filtering options",
470
- inputSchema: zodToJsonSchema(ListGroupProjectsSchema),
471
- },
472
- {
473
- name: "list_wiki_pages",
474
- description: "List wiki pages in a GitLab project",
475
- inputSchema: zodToJsonSchema(ListWikiPagesSchema),
476
- },
477
- {
478
- name: "get_wiki_page",
479
- description: "Get details of a specific wiki page",
480
- inputSchema: zodToJsonSchema(GetWikiPageSchema),
481
- },
482
- {
483
- name: "create_wiki_page",
484
- description: "Create a new wiki page in a GitLab project",
485
- inputSchema: zodToJsonSchema(CreateWikiPageSchema),
486
- },
487
- {
488
- name: "update_wiki_page",
489
- description: "Update an existing wiki page in a GitLab project",
490
- inputSchema: zodToJsonSchema(UpdateWikiPageSchema),
491
- },
492
- {
493
- name: "delete_wiki_page",
494
- description: "Delete a wiki page from a GitLab project",
495
- inputSchema: zodToJsonSchema(DeleteWikiPageSchema),
496
- },
497
- {
498
- name: "get_repository_tree",
499
- description: "Get the repository tree for a GitLab project (list files and directories)",
500
- inputSchema: zodToJsonSchema(GetRepositoryTreeSchema),
501
- },
502
- {
503
- name: "list_pipelines",
504
- description: "List pipelines in a GitLab project with filtering options",
505
- inputSchema: zodToJsonSchema(ListPipelinesSchema),
506
- },
507
- {
508
- name: "get_pipeline",
509
- description: "Get details of a specific pipeline in a GitLab project",
510
- inputSchema: zodToJsonSchema(GetPipelineSchema),
511
- },
512
- {
513
- name: "list_pipeline_jobs",
514
- description: "List all jobs in a specific pipeline",
515
- inputSchema: zodToJsonSchema(ListPipelineJobsSchema),
516
- },
517
- {
518
- name: "list_pipeline_trigger_jobs",
519
- description: "List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines",
520
- inputSchema: zodToJsonSchema(ListPipelineTriggerJobsSchema),
521
- },
522
- {
523
- name: "get_pipeline_job",
524
- description: "Get details of a GitLab pipeline job number",
525
- inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
526
- },
527
- {
528
- name: "get_pipeline_job_output",
529
- description: "Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage",
530
- inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
531
- },
532
- {
533
- name: "create_pipeline",
534
- description: "Create a new pipeline for a branch or tag",
535
- inputSchema: zodToJsonSchema(CreatePipelineSchema),
536
- },
537
- {
538
- name: "retry_pipeline",
539
- description: "Retry a failed or canceled pipeline",
540
- inputSchema: zodToJsonSchema(RetryPipelineSchema),
541
- },
542
- {
543
- name: "cancel_pipeline",
544
- description: "Cancel a running pipeline",
545
- inputSchema: zodToJsonSchema(CancelPipelineSchema),
546
- },
547
- {
548
- name: "list_merge_requests",
549
- description: "List merge requests in a GitLab project with filtering options",
550
- inputSchema: zodToJsonSchema(ListMergeRequestsSchema),
551
- },
552
- {
553
- name: "list_milestones",
554
- description: "List milestones in a GitLab project with filtering options",
555
- inputSchema: zodToJsonSchema(ListProjectMilestonesSchema),
556
- },
557
- {
558
- name: "get_milestone",
559
- description: "Get details of a specific milestone",
560
- inputSchema: zodToJsonSchema(GetProjectMilestoneSchema),
561
- },
562
- {
563
- name: "create_milestone",
564
- description: "Create a new milestone in a GitLab project",
565
- inputSchema: zodToJsonSchema(CreateProjectMilestoneSchema),
566
- },
567
- {
568
- name: "edit_milestone",
569
- description: "Edit an existing milestone in a GitLab project",
570
- inputSchema: zodToJsonSchema(EditProjectMilestoneSchema),
571
- },
572
- {
573
- name: "delete_milestone",
574
- description: "Delete a milestone from a GitLab project",
575
- inputSchema: zodToJsonSchema(DeleteProjectMilestoneSchema),
576
- },
577
- {
578
- name: "get_milestone_issue",
579
- description: "Get issues associated with a specific milestone",
580
- inputSchema: zodToJsonSchema(GetMilestoneIssuesSchema),
581
- },
582
- {
583
- name: "get_milestone_merge_requests",
584
- description: "Get merge requests associated with a specific milestone",
585
- inputSchema: zodToJsonSchema(GetMilestoneMergeRequestsSchema),
586
- },
587
- {
588
- name: "promote_milestone",
589
- description: "Promote a milestone to the next stage",
590
- inputSchema: zodToJsonSchema(PromoteProjectMilestoneSchema),
591
- },
592
- {
593
- name: "get_milestone_burndown_events",
594
- description: "Get burndown events for a specific milestone",
595
- inputSchema: zodToJsonSchema(GetMilestoneBurndownEventsSchema),
596
- },
597
- {
598
- name: "get_users",
599
- description: "Get GitLab user details by usernames",
600
- inputSchema: zodToJsonSchema(GetUsersSchema),
601
- },
602
- {
603
- name: "list_commits",
604
- description: "List repository commits with filtering options",
605
- inputSchema: zodToJsonSchema(ListCommitsSchema),
606
- },
607
- {
608
- name: "get_commit",
609
- description: "Get details of a specific commit",
610
- inputSchema: zodToJsonSchema(GetCommitSchema),
611
- },
612
- {
613
- name: "get_commit_diff",
614
- description: "Get changes/diffs of a specific commit",
615
- inputSchema: zodToJsonSchema(GetCommitDiffSchema),
616
- },
617
- {
618
- name: "list_group_iterations",
619
- description: "List group iterations with filtering options",
620
- inputSchema: zodToJsonSchema(ListGroupIterationsSchema),
621
- },
622
- {
623
- name: "upload_markdown",
624
- description: "Upload a file to a GitLab project for use in markdown content",
625
- inputSchema: zodToJsonSchema(MarkdownUploadSchema),
626
- },
627
- {
628
- name: "download_attachment",
629
- description: "Download an uploaded file from a GitLab project by secret and filename",
630
- inputSchema: zodToJsonSchema(DownloadAttachmentSchema),
631
- },
632
- ];
633
- // Define which tools are read-only
634
- const readOnlyTools = [
635
- "search_repositories",
636
- "get_file_contents",
637
- "get_merge_request",
638
- "get_merge_request_diffs",
639
- "get_branch_diffs",
640
- "mr_discussions",
641
- "list_issues",
642
- "my_issues",
643
- "list_merge_requests",
644
- "get_issue",
645
- "list_issue_links",
646
- "list_issue_discussions",
647
- "get_issue_link",
648
- "list_namespaces",
649
- "get_namespace",
650
- "verify_namespace",
651
- "get_project",
652
- "list_projects",
653
- "list_project_members",
654
- "get_pipeline",
655
- "list_pipelines",
656
- "list_pipeline_jobs",
657
- "list_pipeline_trigger_jobs",
658
- "get_pipeline_job",
659
- "get_pipeline_job_output",
660
- "list_labels",
661
- "get_label",
662
- "list_group_projects",
663
- "get_repository_tree",
664
- "list_milestones",
665
- "get_milestone",
666
- "get_milestone_issue",
667
- "get_milestone_merge_requests",
668
- "get_milestone_burndown_events",
669
- "list_wiki_pages",
670
- "get_wiki_page",
671
- "get_users",
672
- "list_commits",
673
- "get_commit",
674
- "get_commit_diff",
675
- "list_group_iterations",
676
- "get_group_iteration",
677
- "download_attachment",
678
- ];
679
- // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
680
- const wikiToolNames = [
681
- "list_wiki_pages",
682
- "get_wiki_page",
683
- "create_wiki_page",
684
- "update_wiki_page",
685
- "delete_wiki_page",
686
- "upload_wiki_attachment",
687
- ];
688
- // Define which tools are related to milestones and can be toggled by USE_MILESTONE
689
- const milestoneToolNames = [
690
- "list_milestones",
691
- "get_milestone",
692
- "create_milestone",
693
- "edit_milestone",
694
- "delete_milestone",
695
- "get_milestone_issue",
696
- "get_milestone_merge_requests",
697
- "promote_milestone",
698
- "get_milestone_burndown_events",
699
- ];
700
- // Define which tools are related to pipelines and can be toggled by USE_PIPELINE
701
- const pipelineToolNames = [
702
- "list_pipelines",
703
- "get_pipeline",
704
- "list_pipeline_jobs",
705
- "list_pipeline_trigger_jobs",
706
- "get_pipeline_job",
707
- "get_pipeline_job_output",
708
- "create_pipeline",
709
- "retry_pipeline",
710
- "cancel_pipeline",
711
- ];
712
- /**
713
- * Smart URL handling for GitLab API
714
- *
715
- * @param {string | undefined} url - Input GitLab API URL
716
- * @returns {string} Normalized GitLab API URL with /api/v4 path
717
- */
718
- function normalizeGitLabApiUrl(url) {
719
- if (!url) {
720
- return "https://gitlab.com/api/v4";
721
- }
722
- // Remove trailing slash if present
723
- let normalizedUrl = url.endsWith("/") ? url.slice(0, -1) : url;
724
- // Check if URL already has /api/v4
725
- if (!normalizedUrl.endsWith("/api/v4") && !normalizedUrl.endsWith("/api/v4/")) {
726
- // Append /api/v4 if not already present
727
- normalizedUrl = `${normalizedUrl}/api/v4`;
728
- }
729
- return normalizedUrl;
730
- }
731
- // Use the normalizeGitLabApiUrl function to handle various URL formats
732
- const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || "");
733
- const GITLAB_PROJECT_ID = process.env.GITLAB_PROJECT_ID;
734
- const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split(',').map(id => id.trim()).filter(Boolean) || [];
735
- if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
736
- logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
737
- process.exit(1);
738
- }
739
- /**
740
- * Utility function for handling GitLab API errors
741
- * API 에러 처리를 위한 유틸리티 함수 (Utility function for handling API errors)
742
- *
743
- * @param {import("node-fetch").Response} response - The response from GitLab API
744
- * @throws {Error} Throws an error with response details if the request failed
745
- */
746
- async function handleGitLabError(response) {
747
- if (!response.ok) {
748
- const errorBody = await response.text();
749
- // Check specifically for Rate Limit error
750
- if (response.status === 403 && errorBody.includes("User API Key Rate limit exceeded")) {
751
- logger.error("GitLab API Rate Limit Exceeded:", errorBody);
752
- logger.error("User API Key Rate limit exceeded. Please try again later.");
753
- throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
754
- }
755
- else {
756
- // Handle other API errors
757
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
758
- }
759
- }
760
- }
761
- /**
762
- * @param {string} projectId - The project ID parameter passed to the function
763
- * @returns {string} The project ID to use for the API call
764
- * @throws {Error} If GITLAB_ALLOWED_PROJECT_IDS is set and the requested project is not in the whitelist
765
- */
766
- function getEffectiveProjectId(projectId) {
767
- if (GITLAB_ALLOWED_PROJECT_IDS.length > 0) {
768
- // If there's only one allowed project, use it as default
769
- if (GITLAB_ALLOWED_PROJECT_IDS.length === 1 && !projectId) {
770
- return GITLAB_ALLOWED_PROJECT_IDS[0];
771
- }
772
- // If a project ID is provided, check if it's in the whitelist
773
- if (projectId && !GITLAB_ALLOWED_PROJECT_IDS.includes(projectId)) {
774
- throw new Error(`Access denied: Project ${projectId} is not in the allowed project list: ${GITLAB_ALLOWED_PROJECT_IDS.join(', ')}`);
775
- }
776
- // If no project ID provided but we have multiple allowed projects, require an explicit choice
777
- if (!projectId && GITLAB_ALLOWED_PROJECT_IDS.length > 1) {
778
- throw new Error(`Multiple projects allowed (${GITLAB_ALLOWED_PROJECT_IDS.join(', ')}). Please specify a project ID.`);
779
- }
780
- return projectId || GITLAB_ALLOWED_PROJECT_IDS[0];
781
- }
782
- return GITLAB_PROJECT_ID || projectId;
783
- }
784
- /**
785
- * Create a fork of a GitLab project
786
- * 프로젝트 포크 생성 (Create a project fork)
787
- *
788
- * @param {string} projectId - The ID or URL-encoded path of the project
789
- * @param {string} [namespace] - The namespace to fork the project to
790
- * @returns {Promise<GitLabFork>} The created fork
791
- */
792
- async function forkProject(projectId, namespace) {
793
- projectId = decodeURIComponent(projectId); // Decode project ID
794
- const effectiveProjectId = getEffectiveProjectId(projectId);
795
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/fork`);
796
- if (namespace) {
797
- url.searchParams.append("namespace", namespace);
798
- }
799
- const response = await fetch(url.toString(), {
800
- ...DEFAULT_FETCH_CONFIG,
801
- method: "POST",
802
- });
803
- // 이미 존재하는 프로젝트인 경우 처리
804
- if (response.status === 409) {
805
- throw new Error("Project already exists in the target namespace");
806
- }
807
- await handleGitLabError(response);
808
- const data = await response.json();
809
- return GitLabForkSchema.parse(data);
810
- }
811
- /**
812
- * Create a new branch in a GitLab project
813
- * 새로운 브랜치 생성 (Create a new branch)
814
- *
815
- * @param {string} projectId - The ID or URL-encoded path of the project
816
- * @param {z.infer<typeof CreateBranchOptionsSchema>} options - Branch creation options
817
- * @returns {Promise<GitLabReference>} The created branch reference
818
- */
819
- async function createBranch(projectId, options) {
820
- projectId = decodeURIComponent(projectId); // Decode project ID
821
- const effectiveProjectId = getEffectiveProjectId(projectId);
822
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/branches`);
823
- const response = await fetch(url.toString(), {
824
- ...DEFAULT_FETCH_CONFIG,
825
- method: "POST",
826
- body: JSON.stringify({
827
- branch: options.name,
828
- ref: options.ref,
829
- }),
830
- });
831
- await handleGitLabError(response);
832
- return GitLabReferenceSchema.parse(await response.json());
833
- }
834
- /**
835
- * Get the default branch for a GitLab project
836
- * 프로젝트의 기본 브랜치 조회 (Get the default branch of a project)
837
- *
838
- * @param {string} projectId - The ID or URL-encoded path of the project
839
- * @returns {Promise<string>} The name of the default branch
840
- */
841
- async function getDefaultBranchRef(projectId) {
842
- projectId = decodeURIComponent(projectId); // Decode project ID
843
- const effectiveProjectId = getEffectiveProjectId(projectId);
844
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}`);
845
- const response = await fetch(url.toString(), {
846
- ...DEFAULT_FETCH_CONFIG,
847
- });
848
- await handleGitLabError(response);
849
- const project = GitLabRepositorySchema.parse(await response.json());
850
- return project.default_branch ?? "main";
851
- }
852
- /**
853
- * Get the contents of a file from a GitLab project
854
- * 파일 내용 조회 (Get file contents)
855
- *
856
- * @param {string} projectId - The ID or URL-encoded path of the project
857
- * @param {string} filePath - The path of the file to get
858
- * @param {string} [ref] - The name of the branch, tag or commit
859
- * @returns {Promise<GitLabContent>} The file content
860
- */
861
- async function getFileContents(projectId, filePath, ref) {
862
- projectId = decodeURIComponent(projectId); // Decode project ID
863
- const effectiveProjectId = getEffectiveProjectId(projectId);
864
- const encodedPath = encodeURIComponent(filePath);
865
- // ref가 없는 경우 default branch를 가져옴
866
- if (!ref) {
867
- ref = await getDefaultBranchRef(projectId);
868
- }
869
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/files/${encodedPath}`);
870
- url.searchParams.append("ref", ref);
871
- const response = await fetch(url.toString(), {
872
- ...DEFAULT_FETCH_CONFIG,
873
- });
874
- // 파일을 찾을 수 없는 경우 처리
875
- if (response.status === 404) {
876
- throw new Error(`File not found: ${filePath}`);
877
- }
878
- await handleGitLabError(response);
879
- const data = await response.json();
880
- const parsedData = GitLabContentSchema.parse(data);
881
- // Base64로 인코딩된 파일 내용을 UTF-8로 디코딩
882
- if (!Array.isArray(parsedData) && parsedData.content) {
883
- parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8");
884
- parsedData.encoding = "utf8";
885
- }
886
- return parsedData;
887
- }
888
- /**
889
- * Create a new issue in a GitLab project
890
- * 이슈 생성 (Create an issue)
891
- *
892
- * @param {string} projectId - The ID or URL-encoded path of the project
893
- * @param {z.infer<typeof CreateIssueOptionsSchema>} options - Issue creation options
894
- * @returns {Promise<GitLabIssue>} The created issue
895
- */
896
- async function createIssue(projectId, options) {
897
- projectId = decodeURIComponent(projectId); // Decode project ID
898
- const effectiveProjectId = getEffectiveProjectId(projectId);
899
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
900
- const response = await fetch(url.toString(), {
901
- ...DEFAULT_FETCH_CONFIG,
902
- method: "POST",
903
- body: JSON.stringify({
904
- title: options.title,
905
- description: options.description,
906
- assignee_ids: options.assignee_ids,
907
- milestone_id: options.milestone_id,
908
- labels: options.labels?.join(","),
909
- }),
910
- });
911
- // 잘못된 요청 처리
912
- if (response.status === 400) {
913
- const errorBody = await response.text();
914
- throw new Error(`Invalid request: ${errorBody}`);
915
- }
916
- await handleGitLabError(response);
917
- const data = await response.json();
918
- return GitLabIssueSchema.parse(data);
919
- }
920
- /**
921
- * List issues across all accessible projects or within a specific project
922
- * 프로젝트의 이슈 목록 조회
923
- *
924
- * @param {string} projectId - The ID or URL-encoded path of the project (optional)
925
- * @param {Object} options - Options for listing issues
926
- * @returns {Promise<GitLabIssue[]>} List of issues
927
- */
928
- async function listIssues(projectId, options = {}) {
929
- let url;
930
- if (projectId) {
931
- projectId = decodeURIComponent(projectId); // Decode project ID
932
- const effectiveProjectId = getEffectiveProjectId(projectId);
933
- url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
934
- }
935
- else {
936
- url = new URL(`${GITLAB_API_URL}/issues`);
937
- }
938
- // Add all query parameters
939
- Object.entries(options).forEach(([key, value]) => {
940
- if (value !== undefined) {
941
- const keys = ["labels", "assignee_username"];
942
- if (keys.includes(key)) {
943
- if (Array.isArray(value)) {
944
- // Handle array of labels
945
- value.forEach(label => {
946
- url.searchParams.append(`${key}[]`, label.toString());
947
- });
948
- }
949
- else if (value) {
950
- url.searchParams.append(`${key}[]`, value.toString());
951
- }
952
- }
953
- else {
954
- url.searchParams.append(key, String(value));
955
- }
956
- }
957
- });
958
- const response = await fetch(url.toString(), {
959
- ...DEFAULT_FETCH_CONFIG,
960
- });
961
- await handleGitLabError(response);
962
- const data = await response.json();
963
- return z.array(GitLabIssueSchema).parse(data);
964
- }
965
- /**
966
- * List merge requests in a GitLab project with optional filtering
967
- *
968
- * @param {string} projectId - The ID or URL-encoded path of the project
969
- * @param {Object} options - Optional filtering parameters
970
- * @returns {Promise<GitLabMergeRequest[]>} List of merge requests
971
- */
972
- async function listMergeRequests(projectId, options = {}) {
973
- projectId = decodeURIComponent(projectId); // Decode project ID
974
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests`);
975
- // Add all query parameters
976
- Object.entries(options).forEach(([key, value]) => {
977
- if (value !== undefined) {
978
- if (key === "labels" && Array.isArray(value)) {
979
- // Handle array of labels
980
- url.searchParams.append(key, value.join(","));
981
- }
982
- else {
983
- url.searchParams.append(key, String(value));
984
- }
985
- }
986
- });
987
- const response = await fetch(url.toString(), {
988
- ...DEFAULT_FETCH_CONFIG,
989
- });
990
- await handleGitLabError(response);
991
- const data = await response.json();
992
- return z.array(GitLabMergeRequestSchema).parse(data);
993
- }
994
- /**
995
- * Get a single issue from a GitLab project
996
- * 단일 이슈 조회
997
- *
998
- * @param {string} projectId - The ID or URL-encoded path of the project
999
- * @param {number} issueIid - The internal ID of the project issue
1000
- * @returns {Promise<GitLabIssue>} The issue
1001
- */
1002
- async function getIssue(projectId, issueIid) {
1003
- projectId = decodeURIComponent(projectId); // Decode project ID
1004
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`);
1005
- const response = await fetch(url.toString(), {
1006
- ...DEFAULT_FETCH_CONFIG,
1007
- });
1008
- await handleGitLabError(response);
1009
- const data = await response.json();
1010
- return GitLabIssueSchema.parse(data);
1011
- }
1012
- /**
1013
- * Update an issue in a GitLab project
1014
- * 이슈 업데이트
1015
- *
1016
- * @param {string} projectId - The ID or URL-encoded path of the project
1017
- * @param {number} issueIid - The internal ID of the project issue
1018
- * @param {Object} options - Update options for the issue
1019
- * @returns {Promise<GitLabIssue>} The updated issue
1020
- */
1021
- async function updateIssue(projectId, issueIid, options) {
1022
- projectId = decodeURIComponent(projectId); // Decode project ID
1023
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`);
1024
- // Convert labels array to comma-separated string if present
1025
- const body = { ...options };
1026
- if (body.labels && Array.isArray(body.labels)) {
1027
- body.labels = body.labels.join(",");
1028
- }
1029
- const response = await fetch(url.toString(), {
1030
- ...DEFAULT_FETCH_CONFIG,
1031
- method: "PUT",
1032
- body: JSON.stringify(body),
1033
- });
1034
- await handleGitLabError(response);
1035
- const data = await response.json();
1036
- return GitLabIssueSchema.parse(data);
1037
- }
1038
- /**
1039
- * Delete an issue from a GitLab project
1040
- * 이슈 삭제
1041
- *
1042
- * @param {string} projectId - The ID or URL-encoded path of the project
1043
- * @param {number} issueIid - The internal ID of the project issue
1044
- * @returns {Promise<void>}
1045
- */
1046
- async function deleteIssue(projectId, issueIid) {
1047
- projectId = decodeURIComponent(projectId); // Decode project ID
1048
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`);
1049
- const response = await fetch(url.toString(), {
1050
- ...DEFAULT_FETCH_CONFIG,
1051
- method: "DELETE",
1052
- });
1053
- await handleGitLabError(response);
1054
- }
1055
- /**
1056
- * List all issue links for a specific issue
1057
- * 이슈 관계 목록 조회
1058
- *
1059
- * @param {string} projectId - The ID or URL-encoded path of the project
1060
- * @param {number} issueIid - The internal ID of the project issue
1061
- * @returns {Promise<GitLabIssueWithLinkDetails[]>} List of issues with link details
1062
- */
1063
- async function listIssueLinks(projectId, issueIid) {
1064
- projectId = decodeURIComponent(projectId); // Decode project ID
1065
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links`);
1066
- const response = await fetch(url.toString(), {
1067
- ...DEFAULT_FETCH_CONFIG,
1068
- });
1069
- await handleGitLabError(response);
1070
- const data = await response.json();
1071
- return z.array(GitLabIssueWithLinkDetailsSchema).parse(data);
1072
- }
1073
- /**
1074
- * Get a specific issue link
1075
- * 특정 이슈 관계 조회
1076
- *
1077
- * @param {string} projectId - The ID or URL-encoded path of the project
1078
- * @param {number} issueIid - The internal ID of the project issue
1079
- * @param {number} issueLinkId - The ID of the issue link
1080
- * @returns {Promise<GitLabIssueLink>} The issue link
1081
- */
1082
- async function getIssueLink(projectId, issueIid, issueLinkId) {
1083
- projectId = decodeURIComponent(projectId); // Decode project ID
1084
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links/${issueLinkId}`);
1085
- const response = await fetch(url.toString(), {
1086
- ...DEFAULT_FETCH_CONFIG,
1087
- });
1088
- await handleGitLabError(response);
1089
- const data = await response.json();
1090
- return GitLabIssueLinkSchema.parse(data);
1091
- }
1092
- /**
1093
- * Create an issue link between two issues
1094
- * 이슈 관계 생성
1095
- *
1096
- * @param {string} projectId - The ID or URL-encoded path of the project
1097
- * @param {number} issueIid - The internal ID of the project issue
1098
- * @param {string} targetProjectId - The ID or URL-encoded path of the target project
1099
- * @param {number} targetIssueIid - The internal ID of the target project issue
1100
- * @param {string} linkType - The type of the relation (relates_to, blocks, is_blocked_by)
1101
- * @returns {Promise<GitLabIssueLink>} The created issue link
1102
- */
1103
- async function createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = "relates_to") {
1104
- projectId = decodeURIComponent(projectId); // Decode project ID
1105
- targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well
1106
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links`);
1107
- const response = await fetch(url.toString(), {
1108
- ...DEFAULT_FETCH_CONFIG,
1109
- method: "POST",
1110
- body: JSON.stringify({
1111
- target_project_id: targetProjectId,
1112
- target_issue_iid: targetIssueIid,
1113
- link_type: linkType,
1114
- }),
1115
- });
1116
- await handleGitLabError(response);
1117
- const data = await response.json();
1118
- return GitLabIssueLinkSchema.parse(data);
1119
- }
1120
- /**
1121
- * Delete an issue link
1122
- * 이슈 관계 삭제
1123
- *
1124
- * @param {string} projectId - The ID or URL-encoded path of the project
1125
- * @param {number} issueIid - The internal ID of the project issue
1126
- * @param {number} issueLinkId - The ID of the issue link
1127
- * @returns {Promise<void>}
1128
- */
1129
- async function deleteIssueLink(projectId, issueIid, issueLinkId) {
1130
- projectId = decodeURIComponent(projectId); // Decode project ID
1131
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links/${issueLinkId}`);
1132
- const response = await fetch(url.toString(), {
1133
- ...DEFAULT_FETCH_CONFIG,
1134
- method: "DELETE",
1135
- });
1136
- await handleGitLabError(response);
1137
- }
1138
- /**
1139
- * Create a new merge request in a GitLab project
1140
- * 병합 요청 생성
1141
- *
1142
- * @param {string} projectId - The ID or URL-encoded path of the project
1143
- * @param {z.infer<typeof CreateMergeRequestOptionsSchema>} options - Merge request creation options
1144
- * @returns {Promise<GitLabMergeRequest>} The created merge request
1145
- */
1146
- async function createMergeRequest(projectId, options) {
1147
- projectId = decodeURIComponent(projectId); // Decode project ID
1148
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests`);
1149
- const response = await fetch(url.toString(), {
1150
- ...DEFAULT_FETCH_CONFIG,
1151
- method: "POST",
1152
- body: JSON.stringify({
1153
- title: options.title,
1154
- description: options.description,
1155
- source_branch: options.source_branch,
1156
- target_branch: options.target_branch,
1157
- target_project_id: options.target_project_id,
1158
- assignee_ids: options.assignee_ids,
1159
- reviewer_ids: options.reviewer_ids,
1160
- labels: options.labels?.join(","),
1161
- allow_collaboration: options.allow_collaboration,
1162
- draft: options.draft,
1163
- remove_source_branch: options.remove_source_branch,
1164
- squash: options.squash,
1165
- }),
1166
- });
1167
- if (response.status === 400) {
1168
- const errorBody = await response.text();
1169
- throw new Error(`Invalid request: ${errorBody}`);
1170
- }
1171
- if (!response.ok) {
1172
- const errorBody = await response.text();
1173
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1174
- }
1175
- const data = await response.json();
1176
- return GitLabMergeRequestSchema.parse(data);
1177
- }
1178
- /**
1179
- * Shared helper function for listing discussions
1180
- * 토론 목록 조회를 위한 공유 헬퍼 함수
1181
- *
1182
- * @param {string} projectId - The ID or URL-encoded path of the project
1183
- * @param {"issues" | "merge_requests"} resourceType - The type of resource (issues or merge_requests)
1184
- * @param {number} resourceIid - The IID of the issue or merge request
1185
- * @param {PaginationOptions} options - Pagination and sorting options
1186
- * @returns {Promise<PaginatedDiscussionsResponse>} Paginated list of discussions
1187
- */
1188
- async function listDiscussions(projectId, resourceType, resourceIid, options = {}) {
1189
- projectId = decodeURIComponent(projectId); // Decode project ID
1190
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/${resourceType}/${resourceIid}/discussions`);
1191
- // Add query parameters for pagination and sorting
1192
- if (options.page) {
1193
- url.searchParams.append("page", options.page.toString());
1194
- }
1195
- if (options.per_page) {
1196
- url.searchParams.append("per_page", options.per_page.toString());
1197
- }
1198
- const response = await fetch(url.toString(), {
1199
- ...DEFAULT_FETCH_CONFIG,
1200
- });
1201
- await handleGitLabError(response);
1202
- const discussions = await response.json();
1203
- // Extract pagination headers
1204
- const pagination = {
1205
- x_next_page: response.headers.get("x-next-page")
1206
- ? parseInt(response.headers.get("x-next-page"))
1207
- : null,
1208
- x_page: response.headers.get("x-page") ? parseInt(response.headers.get("x-page")) : undefined,
1209
- x_per_page: response.headers.get("x-per-page")
1210
- ? parseInt(response.headers.get("x-per-page"))
1211
- : undefined,
1212
- x_prev_page: response.headers.get("x-prev-page")
1213
- ? parseInt(response.headers.get("x-prev-page"))
1214
- : null,
1215
- x_total: response.headers.get("x-total") ? parseInt(response.headers.get("x-total")) : null,
1216
- x_total_pages: response.headers.get("x-total-pages")
1217
- ? parseInt(response.headers.get("x-total-pages"))
1218
- : null,
1219
- };
1220
- return PaginatedDiscussionsResponseSchema.parse({
1221
- items: discussions,
1222
- pagination: pagination,
1223
- });
1224
- }
1225
- /**
1226
- * List merge request discussion items
1227
- * 병합 요청 토론 목록 조회
1228
- *
1229
- * @param {string} projectId - The ID or URL-encoded path of the project
1230
- * @param {number} mergeRequestIid - The IID of a merge request
1231
- * @param {DiscussionPaginationOptions} options - Pagination and sorting options
1232
- * @returns {Promise<GitLabDiscussion[]>} List of discussions
1233
- */
1234
- async function listMergeRequestDiscussions(projectId, mergeRequestIid, options = {}) {
1235
- return listDiscussions(projectId, "merge_requests", mergeRequestIid, options);
1236
- }
1237
- /**
1238
- * List discussions for an issue
1239
- *
1240
- * @param {string} projectId - The ID or URL-encoded path of the project
1241
- * @param {number} issueIid - The internal ID of the project issue
1242
- * @param {DiscussionPaginationOptions} options - Pagination and sorting options
1243
- * @returns {Promise<GitLabDiscussion[]>} List of issue discussions
1244
- */
1245
- async function listIssueDiscussions(projectId, issueIid, options = {}) {
1246
- return listDiscussions(projectId, "issues", issueIid, options);
1247
- }
1248
- /**
1249
- * Modify an existing merge request thread note
1250
- * 병합 요청 토론 노트 수정
1251
- *
1252
- * @param {string} projectId - The ID or URL-encoded path of the project
1253
- * @param {number} mergeRequestIid - The IID of a merge request
1254
- * @param {string} discussionId - The ID of a thread
1255
- * @param {number} noteId - The ID of a thread note
1256
- * @param {string} body - The new content of the note
1257
- * @param {boolean} [resolved] - Resolve/unresolve state
1258
- * @returns {Promise<GitLabDiscussionNote>} The updated note
1259
- */
1260
- async function updateMergeRequestNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
1261
- projectId = decodeURIComponent(projectId); // Decode project ID
1262
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
1263
- // Only one of body or resolved can be sent according to GitLab API
1264
- const payload = {};
1265
- if (body !== undefined) {
1266
- payload.body = body;
1267
- }
1268
- else if (resolved !== undefined) {
1269
- payload.resolved = resolved;
1270
- }
1271
- const response = await fetch(url.toString(), {
1272
- ...DEFAULT_FETCH_CONFIG,
1273
- method: "PUT",
1274
- body: JSON.stringify(payload),
1275
- });
1276
- await handleGitLabError(response);
1277
- const data = await response.json();
1278
- return GitLabDiscussionNoteSchema.parse(data);
1279
- }
1280
- /**
1281
- * Update an issue discussion note
1282
- * @param {string} projectId - The ID or URL-encoded path of the project
1283
- * @param {number} issueIid - The IID of an issue
1284
- * @param {string} discussionId - The ID of a thread
1285
- * @param {number} noteId - The ID of a thread note
1286
- * @param {string} body - The new content of the note
1287
- * @returns {Promise<GitLabDiscussionNote>} The updated note
1288
- */
1289
- async function updateIssueNote(projectId, issueIid, discussionId, noteId, body) {
1290
- projectId = decodeURIComponent(projectId); // Decode project ID
1291
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/discussions/${discussionId}/notes/${noteId}`);
1292
- const payload = { body };
1293
- const response = await fetch(url.toString(), {
1294
- ...DEFAULT_FETCH_CONFIG,
1295
- method: "PUT",
1296
- body: JSON.stringify(payload),
1297
- });
1298
- await handleGitLabError(response);
1299
- const data = await response.json();
1300
- return GitLabDiscussionNoteSchema.parse(data);
1301
- }
1302
- /**
1303
- * Create a note in an issue discussion
1304
- * @param {string} projectId - The ID or URL-encoded path of the project
1305
- * @param {number} issueIid - The IID of an issue
1306
- * @param {string} discussionId - The ID of a thread
1307
- * @param {string} body - The content of the new note
1308
- * @param {string} [createdAt] - The creation date of the note (ISO 8601 format)
1309
- * @returns {Promise<GitLabDiscussionNote>} The created note
1310
- */
1311
- async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
1312
- projectId = decodeURIComponent(projectId); // Decode project ID
1313
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/discussions/${discussionId}/notes`);
1314
- const payload = { body };
1315
- if (createdAt) {
1316
- payload.created_at = createdAt;
1317
- }
1318
- const response = await fetch(url.toString(), {
1319
- ...DEFAULT_FETCH_CONFIG,
1320
- method: "POST",
1321
- body: JSON.stringify(payload),
1322
- });
1323
- await handleGitLabError(response);
1324
- const data = await response.json();
1325
- return GitLabDiscussionNoteSchema.parse(data);
1326
- }
1327
- /**
1328
- * Add a new note to an existing merge request thread
1329
- * 기존 병합 요청 스레드에 새 노트 추가
1330
- *
1331
- * @param {string} projectId - The ID or URL-encoded path of the project
1332
- * @param {number} mergeRequestIid - The IID of a merge request
1333
- * @param {string} discussionId - The ID of a thread
1334
- * @param {string} body - The content of the new note
1335
- * @param {string} [createdAt] - The creation date of the note (ISO 8601 format)
1336
- * @returns {Promise<GitLabDiscussionNote>} The created note
1337
- */
1338
- async function createMergeRequestNote(projectId, mergeRequestIid, discussionId, body, createdAt) {
1339
- projectId = decodeURIComponent(projectId); // Decode project ID
1340
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes`);
1341
- const payload = { body };
1342
- if (createdAt) {
1343
- payload.created_at = createdAt;
1344
- }
1345
- const response = await fetch(url.toString(), {
1346
- ...DEFAULT_FETCH_CONFIG,
1347
- method: "POST",
1348
- body: JSON.stringify(payload),
1349
- });
1350
- await handleGitLabError(response);
1351
- const data = await response.json();
1352
- return GitLabDiscussionNoteSchema.parse(data);
1353
- }
1354
- /**
1355
- * Create or update a file in a GitLab project
1356
- * 파일 생성 또는 업데이트
1357
- *
1358
- * @param {string} projectId - The ID or URL-encoded path of the project
1359
- * @param {string} filePath - The path of the file to create or update
1360
- * @param {string} content - The content of the file
1361
- * @param {string} commitMessage - The commit message
1362
- * @param {string} branch - The branch name
1363
- * @param {string} [previousPath] - The previous path of the file in case of rename
1364
- * @returns {Promise<GitLabCreateUpdateFileResponse>} The file update response
1365
- */
1366
- async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath, last_commit_id, commit_id) {
1367
- projectId = decodeURIComponent(projectId); // Decode project ID
1368
- const encodedPath = encodeURIComponent(filePath);
1369
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/files/${encodedPath}`);
1370
- const body = {
1371
- branch,
1372
- content,
1373
- commit_message: commitMessage,
1374
- encoding: "text",
1375
- ...(previousPath ? { previous_path: previousPath } : {}),
1376
- };
1377
- // Check if file exists
1378
- let method = "POST";
1379
- try {
1380
- // Get file contents to check existence and retrieve commit IDs
1381
- const fileData = await getFileContents(projectId, filePath, branch);
1382
- method = "PUT";
1383
- // If fileData is not an array, it's a file content object with commit IDs
1384
- if (!Array.isArray(fileData)) {
1385
- // Use commit IDs from the file data if not provided in parameters
1386
- if (!commit_id && fileData.commit_id) {
1387
- body.commit_id = fileData.commit_id;
1388
- }
1389
- else if (commit_id) {
1390
- body.commit_id = commit_id;
1391
- }
1392
- if (!last_commit_id && fileData.last_commit_id) {
1393
- body.last_commit_id = fileData.last_commit_id;
1394
- }
1395
- else if (last_commit_id) {
1396
- body.last_commit_id = last_commit_id;
1397
- }
1398
- }
1399
- }
1400
- catch (error) {
1401
- if (!(error instanceof Error && error.message.includes("File not found"))) {
1402
- throw error;
1403
- }
1404
- // File doesn't exist, use POST - no need for commit IDs for new files
1405
- // But still use any provided as parameters if they exist
1406
- if (commit_id) {
1407
- body.commit_id = commit_id;
1408
- }
1409
- if (last_commit_id) {
1410
- body.last_commit_id = last_commit_id;
1411
- }
1412
- }
1413
- const response = await fetch(url.toString(), {
1414
- ...DEFAULT_FETCH_CONFIG,
1415
- method,
1416
- body: JSON.stringify(body),
1417
- });
1418
- if (!response.ok) {
1419
- const errorBody = await response.text();
1420
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1421
- }
1422
- const data = await response.json();
1423
- return GitLabCreateUpdateFileResponseSchema.parse(data);
1424
- }
1425
- /**
1426
- * Create a tree structure in a GitLab project repository
1427
- * 저장소에 트리 구조 생성
1428
- *
1429
- * @param {string} projectId - The ID or URL-encoded path of the project
1430
- * @param {FileOperation[]} files - Array of file operations
1431
- * @param {string} [ref] - The name of the branch, tag or commit
1432
- * @returns {Promise<GitLabTree>} The created tree
1433
- */
1434
- async function createTree(projectId, files, ref) {
1435
- projectId = decodeURIComponent(projectId); // Decode project ID
1436
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/tree`);
1437
- if (ref) {
1438
- url.searchParams.append("ref", ref);
1439
- }
1440
- const response = await fetch(url.toString(), {
1441
- ...DEFAULT_FETCH_CONFIG,
1442
- method: "POST",
1443
- body: JSON.stringify({
1444
- files: files.map(file => ({
1445
- file_path: file.path,
1446
- content: file.content,
1447
- encoding: "text",
1448
- })),
1449
- }),
1450
- });
1451
- if (response.status === 400) {
1452
- const errorBody = await response.text();
1453
- throw new Error(`Invalid request: ${errorBody}`);
1454
- }
1455
- if (!response.ok) {
1456
- const errorBody = await response.text();
1457
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1458
- }
1459
- const data = await response.json();
1460
- return GitLabTreeSchema.parse(data);
1461
- }
1462
- /**
1463
- * Create a commit in a GitLab project repository
1464
- * 저장소에 커밋 생성
1465
- *
1466
- * @param {string} projectId - The ID or URL-encoded path of the project
1467
- * @param {string} message - The commit message
1468
- * @param {string} branch - The branch name
1469
- * @param {FileOperation[]} actions - Array of file operations for the commit
1470
- * @returns {Promise<GitLabCommit>} The created commit
1471
- */
1472
- async function createCommit(projectId, message, branch, actions) {
1473
- projectId = decodeURIComponent(projectId); // Decode project ID
1474
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits`);
1475
- const response = await fetch(url.toString(), {
1476
- ...DEFAULT_FETCH_CONFIG,
1477
- method: "POST",
1478
- body: JSON.stringify({
1479
- branch,
1480
- commit_message: message,
1481
- actions: actions.map(action => ({
1482
- action: "create",
1483
- file_path: action.path,
1484
- content: action.content,
1485
- encoding: "text",
1486
- })),
1487
- }),
1488
- });
1489
- if (response.status === 400) {
1490
- const errorBody = await response.text();
1491
- throw new Error(`Invalid request: ${errorBody}`);
1492
- }
1493
- if (!response.ok) {
1494
- const errorBody = await response.text();
1495
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1496
- }
1497
- const data = await response.json();
1498
- return GitLabCommitSchema.parse(data);
1499
- }
1500
- /**
1501
- * Search for GitLab projects
1502
- * 프로젝트 검색
1503
- *
1504
- * @param {string} query - The search query
1505
- * @param {number} [page=1] - The page number
1506
- * @param {number} [perPage=20] - Number of items per page
1507
- * @returns {Promise<GitLabSearchResponse>} The search results
1508
- */
1509
- async function searchProjects(query, page = 1, perPage = 20) {
1510
- const url = new URL(`${GITLAB_API_URL}/projects`);
1511
- url.searchParams.append("search", query);
1512
- url.searchParams.append("page", page.toString());
1513
- url.searchParams.append("per_page", perPage.toString());
1514
- url.searchParams.append("order_by", "id");
1515
- url.searchParams.append("sort", "desc");
1516
- const response = await fetch(url.toString(), {
1517
- ...DEFAULT_FETCH_CONFIG,
1518
- });
1519
- if (!response.ok) {
1520
- const errorBody = await response.text();
1521
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1522
- }
1523
- const projects = (await response.json());
1524
- const totalCount = response.headers.get("x-total");
1525
- const totalPages = response.headers.get("x-total-pages");
1526
- // GitLab API doesn't return these headers for results > 10,000
1527
- const count = totalCount ? parseInt(totalCount) : projects.length;
1528
- return GitLabSearchResponseSchema.parse({
1529
- count,
1530
- total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
1531
- current_page: page,
1532
- items: projects,
1533
- });
1534
- }
1535
- /**
1536
- * Create a new GitLab repository
1537
- * 새 저장소 생성
1538
- *
1539
- * @param {z.infer<typeof CreateRepositoryOptionsSchema>} options - Repository creation options
1540
- * @returns {Promise<GitLabRepository>} The created repository
1541
- */
1542
- async function createRepository(options) {
1543
- const response = await fetch(`${GITLAB_API_URL}/projects`, {
1544
- ...DEFAULT_FETCH_CONFIG,
1545
- method: "POST",
1546
- body: JSON.stringify({
1547
- name: options.name,
1548
- description: options.description,
1549
- visibility: options.visibility,
1550
- initialize_with_readme: options.initialize_with_readme,
1551
- default_branch: "main",
1552
- path: options.name.toLowerCase().replace(/\s+/g, "-"),
1553
- }),
1554
- });
1555
- if (!response.ok) {
1556
- const errorBody = await response.text();
1557
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1558
- }
1559
- const data = await response.json();
1560
- return GitLabRepositorySchema.parse(data);
1561
- }
1562
- /**
1563
- * Get merge request details
1564
- * MR 조회 함수 (Function to retrieve merge request)
1565
- *
1566
- * @param {string} projectId - The ID or URL-encoded path of the project
1567
- * @param {number} mergeRequestIid - The internal ID of the merge request (Optional)
1568
- * @param {string} [branchName] - The name of the branch to search for merge request by branch name (Optional)
1569
- * @returns {Promise<GitLabMergeRequest>} The merge request details
1570
- */
1571
- async function getMergeRequest(projectId, mergeRequestIid, branchName) {
1572
- projectId = decodeURIComponent(projectId); // Decode project ID
1573
- let url;
1574
- if (mergeRequestIid) {
1575
- url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
1576
- }
1577
- else if (branchName) {
1578
- url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
1579
- }
1580
- else {
1581
- throw new Error("Either mergeRequestIid or branchName must be provided");
1582
- }
1583
- const response = await fetch(url.toString(), {
1584
- ...DEFAULT_FETCH_CONFIG,
1585
- });
1586
- await handleGitLabError(response);
1587
- const data = await response.json();
1588
- // If response is an array (Comes from branchName search), return the first item if exist
1589
- if (Array.isArray(data) && data.length > 0) {
1590
- return GitLabMergeRequestSchema.parse(data[0]);
1591
- }
1592
- return GitLabMergeRequestSchema.parse(data);
1593
- }
1594
- /**
1595
- * Get merge request changes/diffs
1596
- * MR 변경사항 조회 함수 (Function to retrieve merge request changes)
1597
- *
1598
- * @param {string} projectId - The ID or URL-encoded path of the project
1599
- * @param {number} mergeRequestIid - The internal ID of the merge request (Either mergeRequestIid or branchName must be provided)
1600
- * @param {string} [branchName] - The name of the branch to search for merge request by branch name (Either mergeRequestIid or branchName must be provided)
1601
- * @param {string} [view] - The view type for the diff (inline or parallel)
1602
- * @returns {Promise<GitLabMergeRequestDiff[]>} The merge request diffs
1603
- */
1604
- async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view) {
1605
- projectId = decodeURIComponent(projectId); // Decode project ID
1606
- if (!mergeRequestIid && !branchName) {
1607
- throw new Error("Either mergeRequestIid or branchName must be provided");
1608
- }
1609
- if (branchName && !mergeRequestIid) {
1610
- const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
1611
- mergeRequestIid = mergeRequest.iid;
1612
- }
1613
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/changes`);
1614
- if (view) {
1615
- url.searchParams.append("view", view);
1616
- }
1617
- const response = await fetch(url.toString(), {
1618
- ...DEFAULT_FETCH_CONFIG,
1619
- });
1620
- await handleGitLabError(response);
1621
- const data = (await response.json());
1622
- return z.array(GitLabDiffSchema).parse(data.changes);
1623
- }
1624
- /**
1625
- * Get merge request changes with detailed information including commits, diff_refs, and more
1626
- * 마지막으로 추가된 상세한 MR 변경사항 조회 함수 (Detailed merge request changes retrieval function)
1627
- *
1628
- * @param {string} projectId - The ID or URL-encoded path of the project
1629
- * @param {number} mergeRequestIid - The internal ID of the merge request (Either mergeRequestIid or branchName must be provided)
1630
- * @param {string} [branchName] - The name of the branch to search for merge request by branch name (Either mergeRequestIid or branchName must be provided)
1631
- * @param {boolean} [unidiff] - Return diff in unidiff format
1632
- * @returns {Promise<any>} The complete merge request changes response
1633
- */
1634
- async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, page, perPage, unidiff) {
1635
- projectId = decodeURIComponent(projectId); // Decode project ID
1636
- if (!mergeRequestIid && !branchName) {
1637
- throw new Error("Either mergeRequestIid or branchName must be provided");
1638
- }
1639
- if (branchName && !mergeRequestIid) {
1640
- const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
1641
- mergeRequestIid = mergeRequest.iid;
1642
- }
1643
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/diffs`);
1644
- if (page) {
1645
- url.searchParams.append("page", page.toString());
1646
- }
1647
- if (perPage) {
1648
- url.searchParams.append("per_page", perPage.toString());
1649
- }
1650
- if (unidiff) {
1651
- url.searchParams.append("unidiff", "true");
1652
- }
1653
- const response = await fetch(url.toString(), {
1654
- ...DEFAULT_FETCH_CONFIG,
1655
- });
1656
- await handleGitLabError(response);
1657
- return await response.json(); // Return full response including commits, diff_refs, changes, etc.
1658
- }
1659
- /**
1660
- * Get branch comparison diffs
1661
- *
1662
- * @param {string} projectId - The ID or URL-encoded path of the project
1663
- * @param {string} from - The branch name or commit SHA to compare from
1664
- * @param {string} to - The branch name or commit SHA to compare to
1665
- * @param {boolean} [straight] - Comparison method: false for '...' (default), true for '--'
1666
- * @returns {Promise<GitLabCompareResult>} Branch comparison results
1667
- */
1668
- async function getBranchDiffs(projectId, from, to, straight) {
1669
- projectId = decodeURIComponent(projectId); // Decode project ID
1670
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/compare`);
1671
- url.searchParams.append("from", from);
1672
- url.searchParams.append("to", to);
1673
- if (straight !== undefined) {
1674
- url.searchParams.append("straight", straight.toString());
1675
- }
1676
- const response = await fetch(url.toString(), {
1677
- ...DEFAULT_FETCH_CONFIG,
1678
- });
1679
- if (!response.ok) {
1680
- const errorBody = await response.text();
1681
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1682
- }
1683
- const data = await response.json();
1684
- return GitLabCompareResultSchema.parse(data);
1685
- }
1686
- /**
1687
- * Update a merge request
1688
- * MR 업데이트 함수 (Function to update merge request)
1689
- *
1690
- * @param {string} projectId - The ID or URL-encoded path of the project
1691
- * @param {number} mergeRequestIid - The internal ID of the merge request (Optional)
1692
- * @param {string} branchName - The name of the branch to search for merge request by branch name (Optional)
1693
- * @param {Object} options - The update options
1694
- * @returns {Promise<GitLabMergeRequest>} The updated merge request
1695
- */
1696
- async function updateMergeRequest(projectId, options, mergeRequestIid, branchName) {
1697
- projectId = decodeURIComponent(projectId); // Decode project ID
1698
- if (!mergeRequestIid && !branchName) {
1699
- throw new Error("Either mergeRequestIid or branchName must be provided");
1700
- }
1701
- if (branchName && !mergeRequestIid) {
1702
- const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
1703
- mergeRequestIid = mergeRequest.iid;
1704
- }
1705
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
1706
- const response = await fetch(url.toString(), {
1707
- ...DEFAULT_FETCH_CONFIG,
1708
- method: "PUT",
1709
- body: JSON.stringify(options),
1710
- });
1711
- await handleGitLabError(response);
1712
- return GitLabMergeRequestSchema.parse(await response.json());
1713
- }
1714
- /**
1715
- * Merge a merge request
1716
- * マージリクエストをマージする
1717
- *
1718
- * @param {string} projectId - The ID or URL-encoded path of the project
1719
- * @param {number} mergeRequestIid - The internal ID of the merge request
1720
- * @param {Object} options - Options for merging the merge request
1721
- * @returns {Promise<GitLabMergeRequest>} The merged merge request
1722
- */
1723
- async function mergeMergeRequest(projectId, options, mergeRequestIid) {
1724
- projectId = decodeURIComponent(projectId); // Decode project ID
1725
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/merge`);
1726
- const response = await fetch(url.toString(), {
1727
- ...DEFAULT_FETCH_CONFIG,
1728
- method: "PUT",
1729
- body: JSON.stringify(options),
1730
- });
1731
- await handleGitLabError(response);
1732
- return GitLabMergeRequestSchema.parse(await response.json());
1733
- }
1734
- /**
1735
- * Create a new note (comment) on an issue or merge request
1736
- * 📦 새로운 함수: createNote - 이슈 또는 병합 요청에 노트(댓글)를 추가하는 함수
1737
- * (New function: createNote - Function to add a note (comment) to an issue or merge request)
1738
- *
1739
- * @param {string} projectId - The ID or URL-encoded path of the project
1740
- * @param {"issue" | "merge_request"} noteableType - The type of the item to add a note to (issue or merge_request)
1741
- * @param {number} noteableIid - The internal ID of the issue or merge request
1742
- * @param {string} body - The content of the note
1743
- * @returns {Promise<any>} The created note
1744
- */
1745
- async function createNote(projectId, noteableType, // 'issue' 또는 'merge_request' 타입 명시
1746
- noteableIid, body) {
1747
- projectId = decodeURIComponent(projectId); // Decode project ID
1748
- // ⚙️ 응답 타입은 GitLab API 문서에 따라 조정 가능
1749
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/${noteableType}s/${noteableIid}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation
1750
- );
1751
- const response = await fetch(url.toString(), {
1752
- ...DEFAULT_FETCH_CONFIG,
1753
- method: "POST",
1754
- body: JSON.stringify({ body }),
1755
- });
1756
- if (!response.ok) {
1757
- const errorText = await response.text();
1758
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
1759
- }
1760
- return await response.json();
1761
- }
1762
- /**
1763
- * List draft notes for a merge request
1764
- * @param {string} projectId - The ID or URL-encoded path of the project
1765
- * @param {number|string} mergeRequestIid - The internal ID of the merge request
1766
- * @returns {Promise<GitLabDraftNote[]>} Array of draft notes
1767
- */
1768
- async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
1769
- const response = await fetch(`/projects/${encodeURIComponent(project_id)}/merge_requests/${merge_request_iid}/draft_notes/${draft_note_id}`);
1770
- if (!response.ok) {
1771
- const errorText = await response.text();
1772
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
1773
- }
1774
- const data = await response.json();
1775
- return GitLabDraftNoteSchema.parse(data);
1776
- }
1777
- async function listDraftNotes(projectId, mergeRequestIid) {
1778
- projectId = decodeURIComponent(projectId);
1779
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
1780
- const response = await fetch(url.toString(), {
1781
- ...DEFAULT_FETCH_CONFIG,
1782
- method: "GET",
1783
- });
1784
- if (!response.ok) {
1785
- const errorText = await response.text();
1786
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
1787
- }
1788
- const data = await response.json();
1789
- return z.array(GitLabDraftNoteSchema).parse(data);
1790
- }
1791
- /**
1792
- * Create a draft note for a merge request
1793
- * @param {string} projectId - The ID or URL-encoded path of the project
1794
- * @param {number|string} mergeRequestIid - The internal ID of the merge request
1795
- * @param {string} body - The content of the draft note
1796
- * @param {MergeRequestThreadPosition} [position] - Position information for diff notes
1797
- * @param {boolean} [resolveDiscussion] - Whether to resolve the discussion when publishing
1798
- * @returns {Promise<GitLabDraftNote>} The created draft note
1799
- */
1800
- async function createDraftNote(projectId, mergeRequestIid, body, position, resolveDiscussion) {
1801
- projectId = decodeURIComponent(projectId);
1802
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
1803
- const requestBody = { note: body };
1804
- if (position) {
1805
- requestBody.position = position;
1806
- }
1807
- if (resolveDiscussion !== undefined) {
1808
- requestBody.resolve_discussion = resolveDiscussion;
1809
- }
1810
- const response = await fetch(url.toString(), {
1811
- ...DEFAULT_FETCH_CONFIG,
1812
- method: "POST",
1813
- body: JSON.stringify(requestBody),
1814
- });
1815
- if (!response.ok) {
1816
- const errorText = await response.text();
1817
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
1818
- }
1819
- const data = await response.json();
1820
- return GitLabDraftNoteSchema.parse(data);
1821
- }
1822
- /**
1823
- * Update an existing draft note
1824
- * @param {string} projectId - The ID or URL-encoded path of the project
1825
- * @param {number|string} mergeRequestIid - The internal ID of the merge request
1826
- * @param {number|string} draftNoteId - The ID of the draft note
1827
- * @param {string} [body] - The updated content of the draft note
1828
- * @param {MergeRequestThreadPosition} [position] - Updated position information
1829
- * @param {boolean} [resolveDiscussion] - Whether to resolve the discussion when publishing
1830
- * @returns {Promise<GitLabDraftNote>} The updated draft note
1831
- */
1832
- async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, position, resolveDiscussion) {
1833
- projectId = decodeURIComponent(projectId);
1834
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
1835
- const requestBody = {};
1836
- if (body !== undefined) {
1837
- requestBody.note = body;
1838
- }
1839
- if (position) {
1840
- requestBody.position = position;
1841
- }
1842
- if (resolveDiscussion !== undefined) {
1843
- requestBody.resolve_discussion = resolveDiscussion;
1844
- }
1845
- const response = await fetch(url.toString(), {
1846
- ...DEFAULT_FETCH_CONFIG,
1847
- method: "PUT",
1848
- body: JSON.stringify(requestBody),
1849
- });
1850
- if (!response.ok) {
1851
- const errorText = await response.text();
1852
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
1853
- }
1854
- const data = await response.json();
1855
- return GitLabDraftNoteSchema.parse(data);
1856
- }
1857
- /**
1858
- * Delete a draft note
1859
- * @param {string} projectId - The ID or URL-encoded path of the project
1860
- * @param {number|string} mergeRequestIid - The internal ID of the merge request
1861
- * @param {number|string} draftNoteId - The ID of the draft note
1862
- * @returns {Promise<void>}
1863
- */
1864
- async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
1865
- projectId = decodeURIComponent(projectId);
1866
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
1867
- const response = await fetch(url.toString(), {
1868
- ...DEFAULT_FETCH_CONFIG,
1869
- method: "DELETE",
1870
- });
1871
- if (!response.ok) {
1872
- const errorText = await response.text();
1873
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
1874
- }
1875
- }
1876
- /**
1877
- * Publish a single draft note
1878
- * @param {string} projectId - The ID or URL-encoded path of the project
1879
- * @param {number|string} mergeRequestIid - The internal ID of the merge request
1880
- * @param {number|string} draftNoteId - The ID of the draft note
1881
- * @returns {Promise<GitLabDiscussionNote>} The published note
1882
- */
1883
- async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
1884
- projectId = decodeURIComponent(projectId);
1885
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}/publish`);
1886
- const response = await fetch(url.toString(), {
1887
- ...DEFAULT_FETCH_CONFIG,
1888
- method: "PUT",
1889
- });
1890
- if (!response.ok) {
1891
- const errorText = await response.text();
1892
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
1893
- }
1894
- // Handle empty response (204 No Content) or successful response
1895
- const responseText = await response.text();
1896
- if (!responseText || responseText.trim() === '') {
1897
- // Return a success indicator for empty responses
1898
- return {
1899
- id: draftNoteId.toString(),
1900
- body: "Draft note published successfully",
1901
- author: { id: "unknown", username: "unknown" },
1902
- created_at: new Date().toISOString(),
1903
- updated_at: new Date().toISOString(),
1904
- system: false,
1905
- noteable_id: mergeRequestIid.toString(),
1906
- noteable_type: "MergeRequest"
1907
- };
1908
- }
1909
- try {
1910
- const data = JSON.parse(responseText);
1911
- return GitLabDiscussionNoteSchema.parse(data);
1912
- }
1913
- catch (parseError) {
1914
- // If JSON parsing fails but the operation was successful (2xx status),
1915
- // return a success indicator
1916
- console.warn(`JSON parse error for successful publish operation: ${parseError}`);
1917
- return {
1918
- id: draftNoteId.toString(),
1919
- body: "Draft note published successfully (response parse error)",
1920
- author: { id: "unknown", username: "unknown" },
1921
- created_at: new Date().toISOString(),
1922
- updated_at: new Date().toISOString(),
1923
- system: false,
1924
- noteable_id: mergeRequestIid.toString(),
1925
- noteable_type: "MergeRequest"
1926
- };
1927
- }
1928
- }
1929
- /**
1930
- * Publish all draft notes for a merge request
1931
- * @param {string} projectId - The ID or URL-encoded path of the project
1932
- * @param {number|string} mergeRequestIid - The internal ID of the merge request
1933
- * @returns {Promise<GitLabDiscussionNote[]>} Array of published notes
1934
- */
1935
- async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
1936
- projectId = decodeURIComponent(projectId);
1937
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/bulk_publish`);
1938
- const response = await fetch(url.toString(), {
1939
- ...DEFAULT_FETCH_CONFIG,
1940
- method: "POST", // Changed from PUT to POST
1941
- body: JSON.stringify({}), // Send empty body for POST request
1942
- });
1943
- if (!response.ok) {
1944
- const errorText = await response.text();
1945
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
1946
- }
1947
- // Handle empty response (204 No Content) or successful response
1948
- const responseText = await response.text();
1949
- if (!responseText || responseText.trim() === '') {
1950
- // Return empty array for successful bulk publish with no content
1951
- return [];
1952
- }
1953
- try {
1954
- const data = JSON.parse(responseText);
1955
- return z.array(GitLabDiscussionNoteSchema).parse(data);
1956
- }
1957
- catch (parseError) {
1958
- // If JSON parsing fails but the operation was successful (2xx status),
1959
- // return empty array indicating successful bulk publish
1960
- console.warn(`JSON parse error for successful bulk publish operation: ${parseError}`);
1961
- return [];
1962
- }
1963
- }
1964
- /**
1965
- * Create a new thread on a merge request
1966
- * 📦 새로운 함수: createMergeRequestThread - 병합 요청에 새로운 스레드(토론)를 생성하는 함수
1967
- * (New function: createMergeRequestThread - Function to create a new thread (discussion) on a merge request)
1968
- *
1969
- * This function provides more capabilities than createNote, including the ability to:
1970
- * - Create diff notes (comments on specific lines of code)
1971
- * - Specify exact positions for comments
1972
- * - Set creation timestamps
1973
- *
1974
- * @param {string} projectId - The ID or URL-encoded path of the project
1975
- * @param {number} mergeRequestIid - The internal ID of the merge request
1976
- * @param {string} body - The content of the thread
1977
- * @param {MergeRequestThreadPosition} [position] - Position information for diff notes
1978
- * @param {string} [createdAt] - ISO 8601 formatted creation date
1979
- * @returns {Promise<GitLabDiscussion>} The created discussion thread
1980
- */
1981
- async function createMergeRequestThread(projectId, mergeRequestIid, body, position, createdAt) {
1982
- projectId = decodeURIComponent(projectId); // Decode project ID
1983
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions`);
1984
- const payload = { body };
1985
- // Add optional parameters if provided
1986
- if (position) {
1987
- payload.position = position;
1988
- }
1989
- if (createdAt) {
1990
- payload.created_at = createdAt;
1991
- }
1992
- const response = await fetch(url.toString(), {
1993
- ...DEFAULT_FETCH_CONFIG,
1994
- method: "POST",
1995
- body: JSON.stringify(payload),
1996
- });
1997
- await handleGitLabError(response);
1998
- const data = await response.json();
1999
- return GitLabDiscussionSchema.parse(data);
2000
- }
2001
- /**
2002
- * List all namespaces
2003
- * 사용 가능한 모든 네임스페이스 목록 조회
2004
- *
2005
- * @param {Object} options - Options for listing namespaces
2006
- * @param {string} [options.search] - Search query to filter namespaces
2007
- * @param {boolean} [options.owned_only] - Only return namespaces owned by the authenticated user
2008
- * @param {boolean} [options.top_level_only] - Only return top-level namespaces
2009
- * @returns {Promise<GitLabNamespace[]>} List of namespaces
2010
- */
2011
- async function listNamespaces(options) {
2012
- const url = new URL(`${GITLAB_API_URL}/namespaces`);
2013
- if (options.search) {
2014
- url.searchParams.append("search", options.search);
2015
- }
2016
- if (options.owned_only) {
2017
- url.searchParams.append("owned_only", "true");
2018
- }
2019
- if (options.top_level_only) {
2020
- url.searchParams.append("top_level_only", "true");
2021
- }
2022
- const response = await fetch(url.toString(), {
2023
- ...DEFAULT_FETCH_CONFIG,
2024
- });
2025
- await handleGitLabError(response);
2026
- const data = await response.json();
2027
- return z.array(GitLabNamespaceSchema).parse(data);
2028
- }
2029
- /**
2030
- * Get details on a namespace
2031
- * 네임스페이스 상세 정보 조회
2032
- *
2033
- * @param {string} id - The ID or URL-encoded path of the namespace
2034
- * @returns {Promise<GitLabNamespace>} The namespace details
2035
- */
2036
- async function getNamespace(id) {
2037
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(id)}`);
2038
- const response = await fetch(url.toString(), {
2039
- ...DEFAULT_FETCH_CONFIG,
2040
- });
2041
- await handleGitLabError(response);
2042
- const data = await response.json();
2043
- return GitLabNamespaceSchema.parse(data);
2044
- }
2045
- /**
2046
- * Verify if a namespace exists
2047
- * 네임스페이스 존재 여부 확인
2048
- *
2049
- * @param {string} namespacePath - The path of the namespace to check
2050
- * @param {number} [parentId] - The ID of the parent namespace
2051
- * @returns {Promise<GitLabNamespaceExistsResponse>} The verification result
2052
- */
2053
- async function verifyNamespaceExistence(namespacePath, parentId) {
2054
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(namespacePath)}/exists`);
2055
- if (parentId) {
2056
- url.searchParams.append("parent_id", parentId.toString());
2057
- }
2058
- const response = await fetch(url.toString(), {
2059
- ...DEFAULT_FETCH_CONFIG,
2060
- });
2061
- await handleGitLabError(response);
2062
- const data = await response.json();
2063
- return GitLabNamespaceExistsResponseSchema.parse(data);
2064
- }
2065
- /**
2066
- * Get a single project
2067
- * 단일 프로젝트 조회
2068
- *
2069
- * @param {string} projectId - The ID or URL-encoded path of the project
2070
- * @param {Object} options - Options for getting project details
2071
- * @param {boolean} [options.license] - Include project license data
2072
- * @param {boolean} [options.statistics] - Include project statistics
2073
- * @param {boolean} [options.with_custom_attributes] - Include custom attributes in response
2074
- * @returns {Promise<GitLabProject>} Project details
2075
- */
2076
- async function getProject(projectId, options = {}) {
2077
- projectId = decodeURIComponent(projectId); // Decode project ID
2078
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`);
2079
- if (options.license) {
2080
- url.searchParams.append("license", "true");
2081
- }
2082
- if (options.statistics) {
2083
- url.searchParams.append("statistics", "true");
2084
- }
2085
- if (options.with_custom_attributes) {
2086
- url.searchParams.append("with_custom_attributes", "true");
2087
- }
2088
- const response = await fetch(url.toString(), {
2089
- ...DEFAULT_FETCH_CONFIG,
2090
- });
2091
- await handleGitLabError(response);
2092
- const data = await response.json();
2093
- return GitLabRepositorySchema.parse(data);
2094
- }
2095
- /**
2096
- * List projects
2097
- * 프로젝트 목록 조회
2098
- *
2099
- * @param {Object} options - Options for listing projects
2100
- * @returns {Promise<GitLabProject[]>} List of projects
2101
- */
2102
- async function listProjects(options = {}) {
2103
- // Construct the query parameters
2104
- const params = new URLSearchParams();
2105
- for (const [key, value] of Object.entries(options)) {
2106
- if (value !== undefined && value !== null) {
2107
- if (typeof value === "boolean") {
2108
- params.append(key, value ? "true" : "false");
2109
- }
2110
- else {
2111
- params.append(key, String(value));
2112
- }
2113
- }
2114
- }
2115
- // Make the API request
2116
- const response = await fetch(`${GITLAB_API_URL}/projects?${params.toString()}`, {
2117
- ...DEFAULT_FETCH_CONFIG,
2118
- });
2119
- // Handle errors
2120
- await handleGitLabError(response);
2121
- // Parse and return the data
2122
- const data = await response.json();
2123
- return z.array(GitLabProjectSchema).parse(data);
2124
- }
2125
- /**
2126
- * List labels for a project
2127
- *
2128
- * @param projectId The ID or URL-encoded path of the project
2129
- * @param options Optional parameters for listing labels
2130
- * @returns Array of GitLab labels
2131
- */
2132
- async function listLabels(projectId, options = {}) {
2133
- projectId = decodeURIComponent(projectId); // Decode project ID
2134
- // Construct the URL with project path
2135
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`);
2136
- // Add query parameters
2137
- Object.entries(options).forEach(([key, value]) => {
2138
- if (value !== undefined) {
2139
- if (typeof value === "boolean") {
2140
- url.searchParams.append(key, value ? "true" : "false");
2141
- }
2142
- else {
2143
- url.searchParams.append(key, String(value));
2144
- }
2145
- }
2146
- });
2147
- // Make the API request
2148
- const response = await fetch(url.toString(), {
2149
- ...DEFAULT_FETCH_CONFIG,
2150
- });
2151
- // Handle errors
2152
- await handleGitLabError(response);
2153
- // Parse and return the data
2154
- const data = await response.json();
2155
- return data;
2156
- }
2157
- /**
2158
- * Get a single label from a project
2159
- *
2160
- * @param projectId The ID or URL-encoded path of the project
2161
- * @param labelId The ID or name of the label
2162
- * @param includeAncestorGroups Whether to include ancestor groups
2163
- * @returns GitLab label
2164
- */
2165
- async function getLabel(projectId, labelId, includeAncestorGroups) {
2166
- projectId = decodeURIComponent(projectId); // Decode project ID
2167
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`);
2168
- // Add query parameters
2169
- if (includeAncestorGroups !== undefined) {
2170
- url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false");
2171
- }
2172
- // Make the API request
2173
- const response = await fetch(url.toString(), {
2174
- ...DEFAULT_FETCH_CONFIG,
2175
- });
2176
- // Handle errors
2177
- await handleGitLabError(response);
2178
- // Parse and return the data
2179
- const data = await response.json();
2180
- return data;
2181
- }
2182
- /**
2183
- * Create a new label in a project
2184
- *
2185
- * @param projectId The ID or URL-encoded path of the project
2186
- * @param options Options for creating the label
2187
- * @returns Created GitLab label
2188
- */
2189
- async function createLabel(projectId, options) {
2190
- projectId = decodeURIComponent(projectId); // Decode project ID
2191
- // Make the API request
2192
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`, {
2193
- ...DEFAULT_FETCH_CONFIG,
2194
- method: "POST",
2195
- body: JSON.stringify(options),
2196
- });
2197
- // Handle errors
2198
- await handleGitLabError(response);
2199
- // Parse and return the data
2200
- const data = await response.json();
2201
- return data;
2202
- }
2203
- /**
2204
- * Update an existing label in a project
2205
- *
2206
- * @param projectId The ID or URL-encoded path of the project
2207
- * @param labelId The ID or name of the label to update
2208
- * @param options Options for updating the label
2209
- * @returns Updated GitLab label
2210
- */
2211
- async function updateLabel(projectId, labelId, options) {
2212
- projectId = decodeURIComponent(projectId); // Decode project ID
2213
- // Make the API request
2214
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`, {
2215
- ...DEFAULT_FETCH_CONFIG,
2216
- method: "PUT",
2217
- body: JSON.stringify(options),
2218
- });
2219
- // Handle errors
2220
- await handleGitLabError(response);
2221
- // Parse and return the data
2222
- const data = await response.json();
2223
- return data;
2224
- }
2225
- /**
2226
- * Delete a label from a project
2227
- *
2228
- * @param projectId The ID or URL-encoded path of the project
2229
- * @param labelId The ID or name of the label to delete
2230
- */
2231
- async function deleteLabel(projectId, labelId) {
2232
- projectId = decodeURIComponent(projectId); // Decode project ID
2233
- // Make the API request
2234
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`, {
2235
- ...DEFAULT_FETCH_CONFIG,
2236
- method: "DELETE",
2237
- });
2238
- // Handle errors
2239
- await handleGitLabError(response);
2240
- }
2241
- /**
2242
- * List all projects in a GitLab group
2243
- *
2244
- * @param {z.infer<typeof ListGroupProjectsSchema>} options - Options for listing group projects
2245
- * @returns {Promise<GitLabProject[]>} Array of projects in the group
2246
- */
2247
- async function listGroupProjects(options) {
2248
- const url = new URL(`${GITLAB_API_URL}/groups/${encodeURIComponent(options.group_id)}/projects`);
2249
- // Add optional parameters to URL
2250
- if (options.include_subgroups)
2251
- url.searchParams.append("include_subgroups", "true");
2252
- if (options.search)
2253
- url.searchParams.append("search", options.search);
2254
- if (options.order_by)
2255
- url.searchParams.append("order_by", options.order_by);
2256
- if (options.sort)
2257
- url.searchParams.append("sort", options.sort);
2258
- if (options.page)
2259
- url.searchParams.append("page", options.page.toString());
2260
- if (options.per_page)
2261
- url.searchParams.append("per_page", options.per_page.toString());
2262
- if (options.archived !== undefined)
2263
- url.searchParams.append("archived", options.archived.toString());
2264
- if (options.visibility)
2265
- url.searchParams.append("visibility", options.visibility);
2266
- if (options.with_issues_enabled !== undefined)
2267
- url.searchParams.append("with_issues_enabled", options.with_issues_enabled.toString());
2268
- if (options.with_merge_requests_enabled !== undefined)
2269
- url.searchParams.append("with_merge_requests_enabled", options.with_merge_requests_enabled.toString());
2270
- if (options.min_access_level !== undefined)
2271
- url.searchParams.append("min_access_level", options.min_access_level.toString());
2272
- if (options.with_programming_language)
2273
- url.searchParams.append("with_programming_language", options.with_programming_language);
2274
- if (options.starred !== undefined)
2275
- url.searchParams.append("starred", options.starred.toString());
2276
- if (options.statistics !== undefined)
2277
- url.searchParams.append("statistics", options.statistics.toString());
2278
- if (options.with_custom_attributes !== undefined)
2279
- url.searchParams.append("with_custom_attributes", options.with_custom_attributes.toString());
2280
- if (options.with_security_reports !== undefined)
2281
- url.searchParams.append("with_security_reports", options.with_security_reports.toString());
2282
- const response = await fetch(url.toString(), {
2283
- ...DEFAULT_FETCH_CONFIG,
2284
- });
2285
- await handleGitLabError(response);
2286
- const projects = await response.json();
2287
- return GitLabProjectSchema.array().parse(projects);
2288
- }
2289
- // Wiki API helper functions
2290
- /**
2291
- * List wiki pages in a project
2292
- */
2293
- async function listWikiPages(projectId, options = {}) {
2294
- projectId = decodeURIComponent(projectId); // Decode project ID
2295
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis`);
2296
- if (options.page)
2297
- url.searchParams.append("page", options.page.toString());
2298
- if (options.per_page)
2299
- url.searchParams.append("per_page", options.per_page.toString());
2300
- if (options.with_content)
2301
- url.searchParams.append("with_content", options.with_content.toString());
2302
- const response = await fetch(url.toString(), {
2303
- ...DEFAULT_FETCH_CONFIG,
2304
- });
2305
- await handleGitLabError(response);
2306
- const data = await response.json();
2307
- return GitLabWikiPageSchema.array().parse(data);
2308
- }
2309
- /**
2310
- * Get a specific wiki page
2311
- */
2312
- async function getWikiPage(projectId, slug) {
2313
- projectId = decodeURIComponent(projectId); // Decode project ID
2314
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, { ...DEFAULT_FETCH_CONFIG });
2315
- await handleGitLabError(response);
2316
- const data = await response.json();
2317
- return GitLabWikiPageSchema.parse(data);
2318
- }
2319
- /**
2320
- * Create a new wiki page
2321
- */
2322
- async function createWikiPage(projectId, title, content, format) {
2323
- projectId = decodeURIComponent(projectId); // Decode project ID
2324
- const body = { title, content };
2325
- if (format)
2326
- body.format = format;
2327
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis`, {
2328
- ...DEFAULT_FETCH_CONFIG,
2329
- method: "POST",
2330
- body: JSON.stringify(body),
2331
- });
2332
- await handleGitLabError(response);
2333
- const data = await response.json();
2334
- return GitLabWikiPageSchema.parse(data);
2335
- }
2336
- /**
2337
- * Update an existing wiki page
2338
- */
2339
- async function updateWikiPage(projectId, slug, title, content, format) {
2340
- projectId = decodeURIComponent(projectId); // Decode project ID
2341
- const body = {};
2342
- if (title)
2343
- body.title = title;
2344
- if (content)
2345
- body.content = content;
2346
- if (format)
2347
- body.format = format;
2348
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, {
2349
- ...DEFAULT_FETCH_CONFIG,
2350
- method: "PUT",
2351
- body: JSON.stringify(body),
2352
- });
2353
- await handleGitLabError(response);
2354
- const data = await response.json();
2355
- return GitLabWikiPageSchema.parse(data);
2356
- }
2357
- /**
2358
- * Delete a wiki page
2359
- */
2360
- async function deleteWikiPage(projectId, slug) {
2361
- projectId = decodeURIComponent(projectId); // Decode project ID
2362
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, {
2363
- ...DEFAULT_FETCH_CONFIG,
2364
- method: "DELETE",
2365
- });
2366
- await handleGitLabError(response);
2367
- }
2368
- /**
2369
- * List pipelines in a GitLab project
2370
- *
2371
- * @param {string} projectId - The ID or URL-encoded path of the project
2372
- * @param {ListPipelinesOptions} options - Options for filtering pipelines
2373
- * @returns {Promise<GitLabPipeline[]>} List of pipelines
2374
- */
2375
- async function listPipelines(projectId, options = {}) {
2376
- projectId = decodeURIComponent(projectId); // Decode project ID
2377
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines`);
2378
- // Add all query parameters
2379
- Object.entries(options).forEach(([key, value]) => {
2380
- if (value !== undefined) {
2381
- url.searchParams.append(key, value.toString());
2382
- }
2383
- });
2384
- const response = await fetch(url.toString(), {
2385
- ...DEFAULT_FETCH_CONFIG,
2386
- });
2387
- await handleGitLabError(response);
2388
- const data = await response.json();
2389
- return z.array(GitLabPipelineSchema).parse(data);
2390
- }
2391
- /**
2392
- * Get details of a specific pipeline
2393
- *
2394
- * @param {string} projectId - The ID or URL-encoded path of the project
2395
- * @param {number} pipelineId - The ID of the pipeline
2396
- * @returns {Promise<GitLabPipeline>} Pipeline details
2397
- */
2398
- async function getPipeline(projectId, pipelineId) {
2399
- projectId = decodeURIComponent(projectId); // Decode project ID
2400
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}`);
2401
- const response = await fetch(url.toString(), {
2402
- ...DEFAULT_FETCH_CONFIG,
2403
- });
2404
- if (response.status === 404) {
2405
- throw new Error(`Pipeline not found`);
2406
- }
2407
- await handleGitLabError(response);
2408
- const data = await response.json();
2409
- return GitLabPipelineSchema.parse(data);
2410
- }
2411
- /**
2412
- * List all jobs in a specific pipeline
2413
- *
2414
- * @param {string} projectId - The ID or URL-encoded path of the project
2415
- * @param {number} pipelineId - The ID of the pipeline
2416
- * @param {Object} options - Options for filtering jobs
2417
- * @returns {Promise<GitLabPipelineJob[]>} List of pipeline jobs
2418
- */
2419
- async function listPipelineJobs(projectId, pipelineId, options = {}) {
2420
- projectId = decodeURIComponent(projectId); // Decode project ID
2421
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/jobs`);
2422
- // Add all query parameters
2423
- Object.entries(options).forEach(([key, value]) => {
2424
- if (value !== undefined) {
2425
- if (typeof value === "boolean") {
2426
- url.searchParams.append(key, value ? "true" : "false");
2427
- }
2428
- else {
2429
- url.searchParams.append(key, value.toString());
2430
- }
2431
- }
2432
- });
2433
- const response = await fetch(url.toString(), {
2434
- ...DEFAULT_FETCH_CONFIG,
2435
- });
2436
- if (response.status === 404) {
2437
- throw new Error(`Pipeline not found`);
2438
- }
2439
- await handleGitLabError(response);
2440
- const data = await response.json();
2441
- return z.array(GitLabPipelineJobSchema).parse(data);
2442
- }
2443
- /**
2444
- * List all trigger jobs (bridges) in a specific pipeline
2445
- *
2446
- * @param {string} projectId - The ID or URL-encoded path of the project
2447
- * @param {number} pipelineId - The ID of the pipeline
2448
- * @param {Object} options - Options for filtering trigger jobs
2449
- * @returns {Promise<GitLabPipelineTriggerJob[]>} List of pipeline trigger jobs
2450
- */
2451
- async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
2452
- projectId = decodeURIComponent(projectId); // Decode project ID
2453
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/bridges`);
2454
- // Add all query parameters
2455
- Object.entries(options).forEach(([key, value]) => {
2456
- if (value !== undefined) {
2457
- if (typeof value === "boolean") {
2458
- url.searchParams.append(key, value ? "true" : "false");
2459
- }
2460
- else {
2461
- url.searchParams.append(key, value.toString());
2462
- }
2463
- }
2464
- });
2465
- const response = await fetch(url.toString(), {
2466
- ...DEFAULT_FETCH_CONFIG,
2467
- });
2468
- if (response.status === 404) {
2469
- throw new Error(`Pipeline not found`);
2470
- }
2471
- await handleGitLabError(response);
2472
- const data = await response.json();
2473
- return z.array(GitLabPipelineTriggerJobSchema).parse(data);
2474
- }
2475
- async function getPipelineJob(projectId, jobId) {
2476
- projectId = decodeURIComponent(projectId); // Decode project ID
2477
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}`);
2478
- const response = await fetch(url.toString(), {
2479
- ...DEFAULT_FETCH_CONFIG,
2480
- });
2481
- if (response.status === 404) {
2482
- throw new Error(`Job not found`);
2483
- }
2484
- await handleGitLabError(response);
2485
- const data = await response.json();
2486
- return GitLabPipelineJobSchema.parse(data);
2487
- }
2488
- /**
2489
- * Get the output/trace of a pipeline job
2490
- *
2491
- * @param {string} projectId - The ID or URL-encoded path of the project
2492
- * @param {number} jobId - The ID of the job
2493
- * @param {number} limit - Maximum number of lines to return from the end (default: 1000)
2494
- * @param {number} offset - Number of lines to skip from the end (default: 0)
2495
- * @returns {Promise<string>} The job output/trace
2496
- */
2497
- async function getPipelineJobOutput(projectId, jobId, limit, offset) {
2498
- projectId = decodeURIComponent(projectId); // Decode project ID
2499
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/trace`);
2500
- const response = await fetch(url.toString(), {
2501
- ...DEFAULT_FETCH_CONFIG,
2502
- headers: {
2503
- ...DEFAULT_HEADERS,
2504
- Accept: "text/plain", // Override Accept header to get plain text
2505
- },
2506
- });
2507
- if (response.status === 404) {
2508
- throw new Error(`Job trace not found or job is not finished yet`);
2509
- }
2510
- await handleGitLabError(response);
2511
- const fullTrace = await response.text();
2512
- // Apply client-side pagination to limit context window usage
2513
- if (limit !== undefined || offset !== undefined) {
2514
- const lines = fullTrace.split("\n");
2515
- const startOffset = offset || 0;
2516
- const maxLines = limit || 1000;
2517
- // Return lines from the end, skipping offset lines and limiting to maxLines
2518
- const startIndex = Math.max(0, lines.length - startOffset - maxLines);
2519
- const endIndex = lines.length - startOffset;
2520
- const selectedLines = lines.slice(startIndex, endIndex);
2521
- const result = selectedLines.join("\n");
2522
- // Add metadata about truncation
2523
- if (startIndex > 0 || endIndex < lines.length) {
2524
- const totalLines = lines.length;
2525
- const shownLines = selectedLines.length;
2526
- const skippedFromStart = startIndex;
2527
- const skippedFromEnd = startOffset;
2528
- return `[Log truncated: showing ${shownLines} of ${totalLines} lines, skipped ${skippedFromStart} from start, ${skippedFromEnd} from end]\n\n${result}`;
2529
- }
2530
- return result;
2531
- }
2532
- return fullTrace;
2533
- }
2534
- /**
2535
- * Create a new pipeline
2536
- *
2537
- * @param {string} projectId - The ID or URL-encoded path of the project
2538
- * @param {string} ref - The branch or tag to run the pipeline on
2539
- * @param {Array} variables - Optional variables for the pipeline
2540
- * @returns {Promise<GitLabPipeline>} The created pipeline
2541
- */
2542
- async function createPipeline(projectId, ref, variables) {
2543
- projectId = decodeURIComponent(projectId); // Decode project ID
2544
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline`);
2545
- const body = { ref };
2546
- if (variables && variables.length > 0) {
2547
- body.variables = variables;
2548
- }
2549
- const response = await fetch(url.toString(), {
2550
- method: "POST",
2551
- headers: DEFAULT_HEADERS,
2552
- body: JSON.stringify(body),
2553
- });
2554
- await handleGitLabError(response);
2555
- const data = await response.json();
2556
- return GitLabPipelineSchema.parse(data);
2557
- }
2558
- /**
2559
- * Retry a pipeline
2560
- *
2561
- * @param {string} projectId - The ID or URL-encoded path of the project
2562
- * @param {number} pipelineId - The ID of the pipeline to retry
2563
- * @returns {Promise<GitLabPipeline>} The retried pipeline
2564
- */
2565
- async function retryPipeline(projectId, pipelineId) {
2566
- projectId = decodeURIComponent(projectId); // Decode project ID
2567
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/retry`);
2568
- const response = await fetch(url.toString(), {
2569
- method: "POST",
2570
- headers: DEFAULT_HEADERS,
2571
- });
2572
- await handleGitLabError(response);
2573
- const data = await response.json();
2574
- return GitLabPipelineSchema.parse(data);
2575
- }
2576
- /**
2577
- * Cancel a pipeline
2578
- *
2579
- * @param {string} projectId - The ID or URL-encoded path of the project
2580
- * @param {number} pipelineId - The ID of the pipeline to cancel
2581
- * @returns {Promise<GitLabPipeline>} The canceled pipeline
2582
- */
2583
- async function cancelPipeline(projectId, pipelineId) {
2584
- projectId = decodeURIComponent(projectId); // Decode project ID
2585
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/cancel`);
2586
- const response = await fetch(url.toString(), {
2587
- method: "POST",
2588
- headers: DEFAULT_HEADERS,
2589
- });
2590
- await handleGitLabError(response);
2591
- const data = await response.json();
2592
- return GitLabPipelineSchema.parse(data);
2593
- }
2594
- /**
2595
- * Get the repository tree for a project
2596
- * @param {string} projectId - The ID or URL-encoded path of the project
2597
- * @param {GetRepositoryTreeOptions} options - Options for the tree
2598
- * @returns {Promise<GitLabTreeItem[]>}
2599
- */
2600
- async function getRepositoryTree(options) {
2601
- options.project_id = decodeURIComponent(options.project_id); // Decode project_id within options
2602
- const queryParams = new URLSearchParams();
2603
- if (options.path)
2604
- queryParams.append("path", options.path);
2605
- if (options.ref)
2606
- queryParams.append("ref", options.ref);
2607
- if (options.recursive)
2608
- queryParams.append("recursive", "true");
2609
- if (options.per_page)
2610
- queryParams.append("per_page", options.per_page.toString());
2611
- if (options.page_token)
2612
- queryParams.append("page_token", options.page_token);
2613
- if (options.pagination)
2614
- queryParams.append("pagination", options.pagination);
2615
- const headers = {
2616
- "Content-Type": "application/json",
2617
- };
2618
- if (IS_OLD) {
2619
- headers["Private-Token"] = `${GITLAB_PERSONAL_ACCESS_TOKEN}`;
2620
- }
2621
- else {
2622
- headers["Authorization"] = `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`;
2623
- }
2624
- const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(options.project_id))}/repository/tree?${queryParams.toString()}`, {
2625
- headers,
2626
- });
2627
- if (response.status === 404) {
2628
- throw new Error("Repository or path not found");
2629
- }
2630
- if (!response.ok) {
2631
- throw new Error(`Failed to get repository tree: ${response.statusText}`);
2632
- }
2633
- const data = await response.json();
2634
- return z.array(GitLabTreeItemSchema).parse(data);
2635
- }
2636
- /**
2637
- * List project milestones in a GitLab project
2638
- * @param {string} projectId - The ID or URL-encoded path of the project
2639
- * @param {Object} options - Options for listing milestones
2640
- * @returns {Promise<GitLabMilestones[]>} List of milestones
2641
- */
2642
- async function listProjectMilestones(projectId, options) {
2643
- projectId = decodeURIComponent(projectId);
2644
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`);
2645
- Object.entries(options).forEach(([key, value]) => {
2646
- if (value !== undefined) {
2647
- if (key === "iids" && Array.isArray(value) && value.length > 0) {
2648
- value.forEach(iid => {
2649
- url.searchParams.append("iids[]", iid.toString());
2650
- });
2651
- }
2652
- else if (value !== undefined) {
2653
- url.searchParams.append(key, value.toString());
2654
- }
2655
- }
2656
- });
2657
- const response = await fetch(url.toString(), {
2658
- ...DEFAULT_FETCH_CONFIG,
2659
- });
2660
- await handleGitLabError(response);
2661
- const data = await response.json();
2662
- return z.array(GitLabMilestonesSchema).parse(data);
2663
- }
2664
- /**
2665
- * Get a single milestone in a GitLab project
2666
- * @param {string} projectId - The ID or URL-encoded path of the project
2667
- * @param {number} milestoneId - The ID of the milestone
2668
- * @returns {Promise<GitLabMilestones>} Milestone details
2669
- */
2670
- async function getProjectMilestone(projectId, milestoneId) {
2671
- projectId = decodeURIComponent(projectId);
2672
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}`);
2673
- const response = await fetch(url.toString(), {
2674
- ...DEFAULT_FETCH_CONFIG,
2675
- });
2676
- await handleGitLabError(response);
2677
- const data = await response.json();
2678
- return GitLabMilestonesSchema.parse(data);
2679
- }
2680
- /**
2681
- * Create a new milestone in a GitLab project
2682
- * @param {string} projectId - The ID or URL-encoded path of the project
2683
- * @param {Object} options - Options for creating a milestone
2684
- * @returns {Promise<GitLabMilestones>} Created milestone
2685
- */
2686
- async function createProjectMilestone(projectId, options) {
2687
- projectId = decodeURIComponent(projectId);
2688
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`);
2689
- const response = await fetch(url.toString(), {
2690
- ...DEFAULT_FETCH_CONFIG,
2691
- method: "POST",
2692
- body: JSON.stringify(options),
2693
- });
2694
- await handleGitLabError(response);
2695
- const data = await response.json();
2696
- return GitLabMilestonesSchema.parse(data);
2697
- }
2698
- /**
2699
- * Edit an existing milestone in a GitLab project
2700
- * @param {string} projectId - The ID or URL-encoded path of the project
2701
- * @param {number} milestoneId - The ID of the milestone
2702
- * @param {Object} options - Options for editing a milestone
2703
- * @returns {Promise<GitLabMilestones>} Updated milestone
2704
- */
2705
- async function editProjectMilestone(projectId, milestoneId, options) {
2706
- projectId = decodeURIComponent(projectId);
2707
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}`);
2708
- const response = await fetch(url.toString(), {
2709
- ...DEFAULT_FETCH_CONFIG,
2710
- method: "PUT",
2711
- body: JSON.stringify(options),
2712
- });
2713
- await handleGitLabError(response);
2714
- const data = await response.json();
2715
- return GitLabMilestonesSchema.parse(data);
2716
- }
2717
- /**
2718
- * Delete a milestone from a GitLab project
2719
- * @param {string} projectId - The ID or URL-encoded path of the project
2720
- * @param {number} milestoneId - The ID of the milestone
2721
- * @returns {Promise<void>}
2722
- */
2723
- async function deleteProjectMilestone(projectId, milestoneId) {
2724
- projectId = decodeURIComponent(projectId);
2725
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}`);
2726
- const response = await fetch(url.toString(), {
2727
- ...DEFAULT_FETCH_CONFIG,
2728
- method: "DELETE",
2729
- });
2730
- await handleGitLabError(response);
2731
- }
2732
- /**
2733
- * Get all issues assigned to a single milestone
2734
- * @param {string} projectId - The ID or URL-encoded path of the project
2735
- * @param {number} milestoneId - The ID of the milestone
2736
- * @returns {Promise<GitLabIssue[]>} List of issues
2737
- */
2738
- async function getMilestoneIssues(projectId, milestoneId) {
2739
- projectId = decodeURIComponent(projectId);
2740
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/issues`);
2741
- const response = await fetch(url.toString(), {
2742
- ...DEFAULT_FETCH_CONFIG,
2743
- });
2744
- await handleGitLabError(response);
2745
- const data = await response.json();
2746
- return z.array(GitLabIssueSchema).parse(data);
2747
- }
2748
- /**
2749
- * Get all merge requests assigned to a single milestone
2750
- * @param {string} projectId - The ID or URL-encoded path of the project
2751
- * @param {number} milestoneId - The ID of the milestone
2752
- * @returns {Promise<GitLabMergeRequest[]>} List of merge requests
2753
- */
2754
- async function getMilestoneMergeRequests(projectId, milestoneId) {
2755
- projectId = decodeURIComponent(projectId);
2756
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/merge_requests`);
2757
- const response = await fetch(url.toString(), {
2758
- ...DEFAULT_FETCH_CONFIG,
2759
- });
2760
- await handleGitLabError(response);
2761
- const data = await response.json();
2762
- return z.array(GitLabMergeRequestSchema).parse(data);
2763
- }
2764
- /**
2765
- * Promote a project milestone to a group milestone
2766
- * @param {string} projectId - The ID or URL-encoded path of the project
2767
- * @param {number} milestoneId - The ID of the milestone
2768
- * @returns {Promise<GitLabMilestones>} Promoted milestone
2769
- */
2770
- async function promoteProjectMilestone(projectId, milestoneId) {
2771
- projectId = decodeURIComponent(projectId);
2772
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/promote`);
2773
- const response = await fetch(url.toString(), {
2774
- ...DEFAULT_FETCH_CONFIG,
2775
- method: "POST",
2776
- });
2777
- await handleGitLabError(response);
2778
- const data = await response.json();
2779
- return GitLabMilestonesSchema.parse(data);
2780
- }
2781
- /**
2782
- * Get all burndown chart events for a single milestone
2783
- * @param {string} projectId - The ID or URL-encoded path of the project
2784
- * @param {number} milestoneId - The ID of the milestone
2785
- * @returns {Promise<any[]>} Burndown chart events
2786
- */
2787
- async function getMilestoneBurndownEvents(projectId, milestoneId) {
2788
- projectId = decodeURIComponent(projectId);
2789
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/burndown_events`);
2790
- const response = await fetch(url.toString(), {
2791
- ...DEFAULT_FETCH_CONFIG,
2792
- });
2793
- await handleGitLabError(response);
2794
- const data = await response.json();
2795
- return data;
2796
- }
2797
- /**
2798
- * Get a single user from GitLab
2799
- *
2800
- * @param {string} username - The username to look up
2801
- * @returns {Promise<GitLabUser | null>} The user data or null if not found
2802
- */
2803
- async function getUser(username) {
2804
- try {
2805
- const url = new URL(`${GITLAB_API_URL}/users`);
2806
- url.searchParams.append("username", username);
2807
- const response = await fetch(url.toString(), {
2808
- ...DEFAULT_FETCH_CONFIG,
2809
- });
2810
- await handleGitLabError(response);
2811
- const users = await response.json();
2812
- // GitLab returns an array of users that match the username
2813
- if (Array.isArray(users) && users.length > 0) {
2814
- // Find exact match for username (case-sensitive)
2815
- const exactMatch = users.find(user => user.username === username);
2816
- if (exactMatch) {
2817
- return GitLabUserSchema.parse(exactMatch);
2818
- }
2819
- }
2820
- // No matching user found
2821
- return null;
2822
- }
2823
- catch (error) {
2824
- logger.error(`Error fetching user by username '${username}':`, error);
2825
- return null;
2826
- }
2827
- }
2828
- /**
2829
- * Get multiple users from GitLab
2830
- *
2831
- * @param {string[]} usernames - Array of usernames to look up
2832
- * @returns {Promise<GitLabUsersResponse>} Object with usernames as keys and user objects or null as values
2833
- */
2834
- async function getUsers(usernames) {
2835
- const users = {};
2836
- // Process usernames sequentially to avoid rate limiting
2837
- for (const username of usernames) {
2838
- try {
2839
- const user = await getUser(username);
2840
- users[username] = user;
2841
- }
2842
- catch (error) {
2843
- logger.error(`Error processing username '${username}':`, error);
2844
- users[username] = null;
2845
- }
2846
- }
2847
- return GitLabUsersResponseSchema.parse(users);
2848
- }
2849
- /**
2850
- * List repository commits
2851
- * 저장소 커밋 목록 조회
2852
- *
2853
- * @param {string} projectId - Project ID or URL-encoded path
2854
- * @param {ListCommitsOptions} options - List commits options
2855
- * @returns {Promise<GitLabCommit[]>} List of commits
2856
- */
2857
- async function listCommits(projectId, options = {}) {
2858
- projectId = decodeURIComponent(projectId);
2859
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits`);
2860
- // Add query parameters
2861
- if (options.ref_name)
2862
- url.searchParams.append("ref_name", options.ref_name);
2863
- if (options.since)
2864
- url.searchParams.append("since", options.since);
2865
- if (options.until)
2866
- url.searchParams.append("until", options.until);
2867
- if (options.path)
2868
- url.searchParams.append("path", options.path);
2869
- if (options.author)
2870
- url.searchParams.append("author", options.author);
2871
- if (options.all)
2872
- url.searchParams.append("all", options.all.toString());
2873
- if (options.with_stats)
2874
- url.searchParams.append("with_stats", options.with_stats.toString());
2875
- if (options.first_parent)
2876
- url.searchParams.append("first_parent", options.first_parent.toString());
2877
- if (options.order)
2878
- url.searchParams.append("order", options.order);
2879
- if (options.trailers)
2880
- url.searchParams.append("trailers", options.trailers.toString());
2881
- if (options.page)
2882
- url.searchParams.append("page", options.page.toString());
2883
- if (options.per_page)
2884
- url.searchParams.append("per_page", options.per_page.toString());
2885
- const response = await fetch(url.toString(), {
2886
- ...DEFAULT_FETCH_CONFIG,
2887
- });
2888
- await handleGitLabError(response);
2889
- const data = await response.json();
2890
- return z.array(GitLabCommitSchema).parse(data);
2891
- }
2892
- /**
2893
- * Get a single commit
2894
- * 단일 커밋 정보 조회
2895
- *
2896
- * @param {string} projectId - Project ID or URL-encoded path
2897
- * @param {string} sha - The commit hash or name of a repository branch or tag
2898
- * @param {boolean} [stats] - Include commit stats
2899
- * @returns {Promise<GitLabCommit>} The commit details
2900
- */
2901
- async function getCommit(projectId, sha, stats) {
2902
- projectId = decodeURIComponent(projectId);
2903
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}`);
2904
- if (stats) {
2905
- url.searchParams.append("stats", "true");
2906
- }
2907
- const response = await fetch(url.toString(), {
2908
- ...DEFAULT_FETCH_CONFIG,
2909
- });
2910
- await handleGitLabError(response);
2911
- const data = await response.json();
2912
- return GitLabCommitSchema.parse(data);
2913
- }
2914
- /**
2915
- * Get commit diff
2916
- * 커밋 변경사항 조회
2917
- *
2918
- * @param {string} projectId - Project ID or URL-encoded path
2919
- * @param {string} sha - The commit hash or name of a repository branch or tag
2920
- * @returns {Promise<GitLabMergeRequestDiff[]>} The commit diffs
2921
- */
2922
- async function getCommitDiff(projectId, sha) {
2923
- projectId = decodeURIComponent(projectId);
2924
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}/diff`);
2925
- const response = await fetch(url.toString(), {
2926
- ...DEFAULT_FETCH_CONFIG,
2927
- });
2928
- await handleGitLabError(response);
2929
- const data = await response.json();
2930
- return z.array(GitLabDiffSchema).parse(data);
2931
- }
2932
- /**
2933
- * Get the current authenticated user
2934
- * 현재 인증된 사용자 가져오기
2935
- *
2936
- * @returns {Promise<GitLabUser>} The current user
2937
- */
2938
- async function getCurrentUser() {
2939
- const response = await fetch(`${GITLAB_API_URL}/user`, DEFAULT_FETCH_CONFIG);
2940
- await handleGitLabError(response);
2941
- const data = await response.json();
2942
- return GitLabUserSchema.parse(data);
2943
- }
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
+ import express from "express";
5
+ import { mcpserver } from "./src/mcpserver.js";
6
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
+ import { config, validateConfiguration } from "./src/config.js";
8
+ import { configureAuthentication } from "./src/authentication.js";
9
+ import { logger } from "./src/logger.js";
10
+ import argon2 from "./src/argon2wrapper.js";
11
+ import { randomUUID } from "crypto";
12
+ validateConfiguration();
2944
13
  /**
2945
- * List issues assigned to the current authenticated user
2946
- * 현재 인증된 사용자에게 할당된 이슈 목록 조회
2947
- *
2948
- * @param {MyIssuesOptions} options - Options for filtering issues
2949
- * @returns {Promise<GitLabIssue[]>} List of issues assigned to the current user
14
+ * Determine which transport modes are enabled based on environment variables
15
+ * If both SSE and STREAMABLE_HTTP are disabled, defaults to STDIO
2950
16
  */
2951
- async function myIssues(options = {}) {
2952
- // Get current user to find their username
2953
- const currentUser = await getCurrentUser();
2954
- // Use getEffectiveProjectId to handle project ID resolution
2955
- const effectiveProjectId = getEffectiveProjectId(options.project_id || "");
2956
- // Use listIssues with assignee_username filter
2957
- let listIssuesOptions = {
2958
- state: options.state || "opened", // Default to "opened" if not specified
2959
- labels: options.labels,
2960
- milestone: options.milestone,
2961
- search: options.search,
2962
- created_after: options.created_after,
2963
- created_before: options.created_before,
2964
- updated_after: options.updated_after,
2965
- updated_before: options.updated_before,
2966
- per_page: options.per_page,
2967
- page: options.page,
17
+ function determineTransportModes() {
18
+ const sseEnabled = config.SSE;
19
+ const streamableHttpEnabled = config.STREAMABLE_HTTP;
20
+ // If neither SSE nor STREAMABLE_HTTP are enabled, use STDIO
21
+ const stdioEnabled = !sseEnabled && !streamableHttpEnabled;
22
+ return {
23
+ stdio: stdioEnabled,
24
+ sse: sseEnabled,
25
+ streamableHttp: streamableHttpEnabled
2968
26
  };
2969
- if (currentUser.username) {
2970
- listIssuesOptions.assignee_username = [currentUser.username];
2971
- }
2972
- else {
2973
- listIssuesOptions.assignee_id = currentUser.id;
2974
- }
2975
- return listIssues(effectiveProjectId, listIssuesOptions);
2976
- }
2977
- /**
2978
- * List members of a GitLab project
2979
- * GitLab 프로젝트 멤버 목록 조회
2980
- *
2981
- * @param {string} projectId - Project ID or URL-encoded path
2982
- * @param {Omit<ListProjectMembersOptions, "project_id">} options - Options for filtering members
2983
- * @returns {Promise<GitLabProjectMember[]>} List of project members
2984
- */
2985
- async function listProjectMembers(projectId, options = {}) {
2986
- projectId = decodeURIComponent(projectId);
2987
- const effectiveProjectId = getEffectiveProjectId(projectId);
2988
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/members`);
2989
- // Add query parameters
2990
- if (options.query)
2991
- url.searchParams.append("query", options.query);
2992
- if (options.user_ids) {
2993
- options.user_ids.forEach(id => url.searchParams.append("user_ids[]", id.toString()));
2994
- }
2995
- if (options.skip_users) {
2996
- options.skip_users.forEach(id => url.searchParams.append("skip_users[]", id.toString()));
2997
- }
2998
- if (options.per_page)
2999
- url.searchParams.append("per_page", options.per_page.toString());
3000
- if (options.page)
3001
- url.searchParams.append("page", options.page.toString());
3002
- const response = await fetch(url.toString(), DEFAULT_FETCH_CONFIG);
3003
- await handleGitLabError(response);
3004
- const data = await response.json();
3005
- return z.array(GitLabProjectMemberSchema).parse(data);
3006
- }
3007
- /**
3008
- * list group iterations
3009
- *
3010
- * @param {string} groupId
3011
- * @param {Omit<ListGroupIterationsOptions, "group_id">} options
3012
- * @returns {Promise<GetIt[]>}
3013
- */
3014
- async function listGroupIterations(groupId, options = {}) {
3015
- groupId = decodeURIComponent(groupId);
3016
- const url = new URL(`${GITLAB_API_URL}/groups/${encodeURIComponent(groupId)}/iterations`);
3017
- // クエリパラメータの追加
3018
- if (options.state)
3019
- url.searchParams.append("state", options.state);
3020
- if (options.search)
3021
- url.searchParams.append("search", options.search);
3022
- if (options.in)
3023
- url.searchParams.append("in", options.in.join(","));
3024
- if (options.include_ancestors !== undefined)
3025
- url.searchParams.append("include_ancestors", options.include_ancestors.toString());
3026
- if (options.include_descendants !== undefined)
3027
- url.searchParams.append("include_descendants", options.include_descendants.toString());
3028
- if (options.updated_before)
3029
- url.searchParams.append("updated_before", options.updated_before);
3030
- if (options.updated_after)
3031
- url.searchParams.append("updated_after", options.updated_after);
3032
- if (options.page)
3033
- url.searchParams.append("page", options.page.toString());
3034
- if (options.per_page)
3035
- url.searchParams.append("per_page", options.per_page.toString());
3036
- const response = await fetch(url.toString(), DEFAULT_FETCH_CONFIG);
3037
- if (!response.ok) {
3038
- await handleGitLabError(response);
3039
- }
3040
- const data = await response.json();
3041
- return z.array(GroupIteration).parse(data);
3042
27
  }
3043
28
  /**
3044
- * Upload a file to a GitLab project for use in markdown content
3045
- *
3046
- * @param {string} projectId - The ID or URL-encoded path of the project
3047
- * @param {string} filePath - Path to the local file to upload
3048
- * @returns {Promise<GitLabMarkdownUpload>} The upload response
29
+ * Start server with stdio transport
3049
30
  */
3050
- async function markdownUpload(projectId, filePath) {
3051
- projectId = decodeURIComponent(projectId);
3052
- const effectiveProjectId = getEffectiveProjectId(projectId);
3053
- // Check if file exists
3054
- if (!fs.existsSync(filePath)) {
3055
- throw new Error(`File not found: ${filePath}`);
3056
- }
3057
- // Read the file
3058
- const fileBuffer = fs.readFileSync(filePath);
3059
- const fileName = path.basename(filePath);
3060
- // Create form data
3061
- const FormData = (await import("form-data")).default;
3062
- const form = new FormData();
3063
- form.append("file", fileBuffer, {
3064
- filename: fileName,
3065
- contentType: "application/octet-stream",
3066
- });
3067
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/uploads`);
3068
- const response = await fetch(url.toString(), {
3069
- method: "POST",
3070
- headers: {
3071
- ...DEFAULT_HEADERS,
3072
- // Remove Content-Type header to let form-data set it with boundary
3073
- "Content-Type": undefined,
3074
- },
3075
- body: form,
3076
- });
3077
- if (!response.ok) {
3078
- await handleGitLabError(response);
3079
- }
3080
- const data = await response.json();
3081
- return GitLabMarkdownUploadSchema.parse(data);
3082
- }
3083
- async function downloadAttachment(projectId, secret, filename, localPath) {
3084
- const effectiveProjectId = getEffectiveProjectId(projectId);
3085
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${secret}/${filename}`);
3086
- const response = await fetch(url.toString(), {
3087
- method: "GET",
3088
- headers: DEFAULT_HEADERS,
3089
- });
3090
- if (!response.ok) {
3091
- await handleGitLabError(response);
3092
- }
3093
- // Get the file content as buffer
3094
- const buffer = await response.arrayBuffer();
3095
- // Determine the save path
3096
- const savePath = localPath ? path.join(localPath, filename) : filename;
3097
- // Write the file to disk
3098
- fs.writeFileSync(savePath, Buffer.from(buffer));
3099
- return savePath;
31
+ async function startStdioServer() {
32
+ const transport = new StdioServerTransport();
33
+ await mcpserver.connect(transport);
3100
34
  }
3101
- server.setRequestHandler(ListToolsRequestSchema, async () => {
3102
- // Apply read-only filter first
3103
- const tools0 = GITLAB_READ_ONLY_MODE
3104
- ? allTools.filter(tool => readOnlyTools.includes(tool.name))
3105
- : allTools;
3106
- // Toggle wiki tools by USE_GITLAB_WIKI flag
3107
- const tools1 = USE_GITLAB_WIKI
3108
- ? tools0
3109
- : tools0.filter(tool => !wikiToolNames.includes(tool.name));
3110
- // Toggle milestone tools by USE_MILESTONE flag
3111
- const tools2 = USE_MILESTONE
3112
- ? tools1
3113
- : tools1.filter(tool => !milestoneToolNames.includes(tool.name));
3114
- // Toggle pipeline tools by USE_PIPELINE flag
3115
- let tools = USE_PIPELINE ? tools2 : tools2.filter(tool => !pipelineToolNames.includes(tool.name));
3116
- // <<< START: Gemini 호환성을 위해 $schema 제거 >>>
3117
- tools = tools.map(tool => {
3118
- // inputSchema가 존재하고 객체인지 확인
3119
- if (tool.inputSchema && typeof tool.inputSchema === "object" && tool.inputSchema !== null) {
3120
- // $schema 키가 존재하면 삭제
3121
- if ("$schema" in tool.inputSchema) {
3122
- // 불변성을 위해 새로운 객체 생성 (선택적이지만 권장)
3123
- const modifiedSchema = { ...tool.inputSchema };
3124
- delete modifiedSchema.$schema;
3125
- return { ...tool, inputSchema: modifiedSchema };
3126
- }
3127
- }
3128
- // 변경이 필요 없으면 그대로 반환
3129
- return tool;
3130
- });
3131
- // <<< END: Gemini 호환성을 위해 $schema 제거 >>>
3132
- return {
3133
- tools, // $schema가 제거된 도구 목록 반환
3134
- };
3135
- });
3136
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
3137
- try {
3138
- if (!request.params.arguments) {
3139
- throw new Error("Arguments are required");
3140
- }
3141
- // Ensure session is established for every request if cookie authentication is enabled
3142
- if (GITLAB_AUTH_COOKIE_PATH) {
3143
- await ensureSessionForRequest();
3144
- }
3145
- logger.info(request.params.name);
3146
- switch (request.params.name) {
3147
- case "fork_repository": {
3148
- if (GITLAB_PROJECT_ID) {
3149
- throw new Error("Direct project ID is set. So fork_repository is not allowed");
3150
- }
3151
- const forkArgs = ForkRepositorySchema.parse(request.params.arguments);
3152
- try {
3153
- const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace);
3154
- return {
3155
- content: [{ type: "text", text: JSON.stringify(forkedProject, null, 2) }],
3156
- };
3157
- }
3158
- catch (forkError) {
3159
- logger.error("Error forking repository:", forkError);
3160
- let forkErrorMessage = "Failed to fork repository";
3161
- if (forkError instanceof Error) {
3162
- forkErrorMessage = `${forkErrorMessage}: ${forkError.message}`;
3163
- }
3164
- return {
3165
- content: [
3166
- {
3167
- type: "text",
3168
- text: JSON.stringify({ error: forkErrorMessage }, null, 2),
3169
- },
3170
- ],
3171
- };
3172
- }
3173
- }
3174
- case "create_branch": {
3175
- const args = CreateBranchSchema.parse(request.params.arguments);
3176
- let ref = args.ref;
3177
- if (!ref) {
3178
- ref = await getDefaultBranchRef(args.project_id);
3179
- }
3180
- const branch = await createBranch(args.project_id, {
3181
- name: args.branch,
3182
- ref,
35
+ // used for the sse and streamable http transports to share auth
36
+ async function startExpressServer(options) {
37
+ const { sseEnabled, streamableHttpEnabled } = options;
38
+ const app = express();
39
+ const authMiddleware = await configureAuthentication(app);
40
+ const argon2Salt = new TextEncoder().encode(config.ARGON2_SALT);
41
+ if (sseEnabled) {
42
+ const transports = {};
43
+ app.get("/sse", authMiddleware, async (req, res) => {
44
+ const transport = new SSEServerTransport("/messages", res);
45
+ transports[transport.sessionId] = {
46
+ transport,
47
+ };
48
+ // if we have a valid auth info here, either obtained from the passthrough token or oauth, we tie it to a session.
49
+ if (req.auth) {
50
+ transports[transport.sessionId].tokenHash = await argon2.hash(req.auth.token, {
51
+ salt: argon2Salt,
3183
52
  });
3184
- return {
3185
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
3186
- };
3187
- }
3188
- case "get_branch_diffs": {
3189
- const args = GetBranchDiffsSchema.parse(request.params.arguments);
3190
- const diffResp = await getBranchDiffs(args.project_id, args.from, args.to, args.straight);
3191
- if (args.excluded_file_patterns?.length) {
3192
- const regexPatterns = args.excluded_file_patterns.map(pattern => new RegExp(pattern));
3193
- // Helper function to check if a path matches any regex pattern
3194
- const matchesAnyPattern = (path) => {
3195
- if (!path)
3196
- return false;
3197
- return regexPatterns.some(regex => regex.test(path));
3198
- };
3199
- // Filter out files that match any of the regex patterns on new files
3200
- diffResp.diffs = diffResp.diffs.filter(diff => !matchesAnyPattern(diff.new_path));
3201
- }
3202
- return {
3203
- content: [{ type: "text", text: JSON.stringify(diffResp, null, 2) }],
3204
- };
3205
- }
3206
- case "search_repositories": {
3207
- const args = SearchRepositoriesSchema.parse(request.params.arguments);
3208
- const results = await searchProjects(args.search, args.page, args.per_page);
3209
- return {
3210
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
3211
- };
3212
- }
3213
- case "create_repository": {
3214
- if (GITLAB_PROJECT_ID) {
3215
- throw new Error("Direct project ID is set. So fork_repository is not allowed");
3216
- }
3217
- const args = CreateRepositorySchema.parse(request.params.arguments);
3218
- const repository = await createRepository(args);
3219
- return {
3220
- content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
3221
- };
3222
- }
3223
- case "get_file_contents": {
3224
- const args = GetFileContentsSchema.parse(request.params.arguments);
3225
- const contents = await getFileContents(args.project_id, args.file_path, args.ref);
3226
- return {
3227
- content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
3228
- };
3229
- }
3230
- case "create_or_update_file": {
3231
- const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
3232
- 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);
3233
- return {
3234
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3235
- };
3236
- }
3237
- case "push_files": {
3238
- const args = PushFilesSchema.parse(request.params.arguments);
3239
- const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(f => ({ path: f.file_path, content: f.content })));
3240
- return {
3241
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3242
- };
3243
- }
3244
- case "create_issue": {
3245
- const args = CreateIssueSchema.parse(request.params.arguments);
3246
- const { project_id, ...options } = args;
3247
- const issue = await createIssue(project_id, options);
3248
- return {
3249
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
3250
- };
3251
- }
3252
- case "create_merge_request": {
3253
- const args = CreateMergeRequestSchema.parse(request.params.arguments);
3254
- const { project_id, ...options } = args;
3255
- const mergeRequest = await createMergeRequest(project_id, options);
3256
- return {
3257
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
3258
- };
3259
- }
3260
- case "update_merge_request_note": {
3261
- const args = UpdateMergeRequestNoteSchema.parse(request.params.arguments);
3262
- const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.discussion_id, args.note_id, args.body, // Now optional
3263
- args.resolved // Now one of body or resolved must be provided, not both
3264
- );
3265
- return {
3266
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
3267
- };
3268
- }
3269
- case "create_merge_request_note": {
3270
- const args = CreateMergeRequestNoteSchema.parse(request.params.arguments);
3271
- const note = await createMergeRequestNote(args.project_id, args.merge_request_iid, args.discussion_id, args.body, args.created_at);
3272
- return {
3273
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
3274
- };
53
+ logger.debug({
54
+ tokenHash: transports[transport.sessionId].tokenHash?.slice(-8),
55
+ }, "created new auth session");
3275
56
  }
3276
- case "update_issue_note": {
3277
- const args = UpdateIssueNoteSchema.parse(request.params.arguments);
3278
- const note = await updateIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.note_id, args.body);
3279
- return {
3280
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
3281
- };
3282
- }
3283
- case "create_issue_note": {
3284
- const args = CreateIssueNoteSchema.parse(request.params.arguments);
3285
- const note = await createIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.body, args.created_at);
3286
- return {
3287
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
3288
- };
3289
- }
3290
- case "get_merge_request": {
3291
- const args = GetMergeRequestSchema.parse(request.params.arguments);
3292
- const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid, args.source_branch);
3293
- return {
3294
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
3295
- };
3296
- }
3297
- case "get_merge_request_diffs": {
3298
- const args = GetMergeRequestDiffsSchema.parse(request.params.arguments);
3299
- const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.view);
3300
- return {
3301
- content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
3302
- };
3303
- }
3304
- case "list_merge_request_diffs": {
3305
- const args = ListMergeRequestDiffsSchema.parse(request.params.arguments);
3306
- const changes = await listMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.page, args.per_page, args.unidiff);
3307
- return {
3308
- content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
3309
- };
3310
- }
3311
- case "update_merge_request": {
3312
- const args = UpdateMergeRequestSchema.parse(request.params.arguments);
3313
- const { project_id, merge_request_iid, source_branch, ...options } = args;
3314
- const mergeRequest = await updateMergeRequest(project_id, options, merge_request_iid, source_branch);
3315
- return {
3316
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
3317
- };
57
+ res.on("close", () => {
58
+ delete transports[transport.sessionId];
59
+ });
60
+ try {
61
+ await mcpserver.connect(transport);
3318
62
  }
3319
- case "merge_merge_request": {
3320
- const args = MergeMergeRequestSchema.parse(request.params.arguments);
3321
- const { project_id, merge_request_iid, ...options } = args;
3322
- const mergeRequest = await mergeMergeRequest(project_id, options, merge_request_iid);
3323
- return {
3324
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
3325
- };
63
+ catch (e) {
64
+ logger.error({ e }, "Transport error connecting to MCP server:");
65
+ res.status(500).send("Internal server error");
3326
66
  }
3327
- case "mr_discussions": {
3328
- const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments);
3329
- const { project_id, merge_request_iid, ...options } = args;
3330
- const discussions = await listMergeRequestDiscussions(project_id, merge_request_iid, options);
3331
- return {
3332
- content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
3333
- };
67
+ });
68
+ app.post("/messages", authMiddleware, async (req, res) => {
69
+ const sessionId = req.query.sessionId;
70
+ const transportDetails = transports[sessionId];
71
+ if (!transportDetails) {
72
+ res.status(400).send("No transport found for sessionId");
73
+ return;
3334
74
  }
3335
- case "list_namespaces": {
3336
- const args = ListNamespacesSchema.parse(request.params.arguments);
3337
- const url = new URL(`${GITLAB_API_URL}/namespaces`);
3338
- if (args.search) {
3339
- url.searchParams.append("search", args.search);
75
+ const { transport, tokenHash } = transportDetails;
76
+ // means we have a token hash to verify.
77
+ if (tokenHash) {
78
+ // NOTE: at this point, we assume that this req.auth is a "valid" AuthInfo
79
+ // TODO: consider the security implications of this when verifying dcr clients.
80
+ if (!req.auth) {
81
+ res.status(401).send("No authorization information sent");
82
+ return;
3340
83
  }
3341
- if (args.page) {
3342
- url.searchParams.append("page", args.page.toString());
84
+ const gitlabToken = req.auth.token;
85
+ if (!gitlabToken) {
86
+ res.status(401).send("No valid token info found in request");
87
+ return;
3343
88
  }
3344
- if (args.per_page) {
3345
- url.searchParams.append("per_page", args.per_page.toString());
3346
- }
3347
- if (args.owned) {
3348
- url.searchParams.append("owned", args.owned.toString());
3349
- }
3350
- const response = await fetch(url.toString(), {
3351
- ...DEFAULT_FETCH_CONFIG,
3352
- });
3353
- await handleGitLabError(response);
3354
- const data = await response.json();
3355
- const namespaces = z.array(GitLabNamespaceSchema).parse(data);
3356
- return {
3357
- content: [{ type: "text", text: JSON.stringify(namespaces, null, 2) }],
3358
- };
3359
- }
3360
- case "get_namespace": {
3361
- const args = GetNamespaceSchema.parse(request.params.arguments);
3362
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.namespace_id)}`);
3363
- const response = await fetch(url.toString(), {
3364
- ...DEFAULT_FETCH_CONFIG,
3365
- });
3366
- await handleGitLabError(response);
3367
- const data = await response.json();
3368
- const namespace = GitLabNamespaceSchema.parse(data);
3369
- return {
3370
- content: [{ type: "text", text: JSON.stringify(namespace, null, 2) }],
3371
- };
3372
- }
3373
- case "verify_namespace": {
3374
- const args = VerifyNamespaceSchema.parse(request.params.arguments);
3375
- const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists`);
3376
- const response = await fetch(url.toString(), {
3377
- ...DEFAULT_FETCH_CONFIG,
3378
- });
3379
- await handleGitLabError(response);
3380
- const data = await response.json();
3381
- const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data);
3382
- return {
3383
- content: [{ type: "text", text: JSON.stringify(namespaceExists, null, 2) }],
3384
- };
3385
- }
3386
- case "get_project": {
3387
- const args = GetProjectSchema.parse(request.params.arguments);
3388
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(args.project_id))}`);
3389
- const response = await fetch(url.toString(), {
3390
- ...DEFAULT_FETCH_CONFIG,
89
+ const verified = await argon2.verify(tokenHash, gitlabToken, {
90
+ salt: argon2Salt,
3391
91
  });
3392
- await handleGitLabError(response);
3393
- const data = await response.json();
3394
- const project = GitLabProjectSchema.parse(data);
3395
- return {
3396
- content: [{ type: "text", text: JSON.stringify(project, null, 2) }],
3397
- };
3398
- }
3399
- case "list_projects": {
3400
- const args = ListProjectsSchema.parse(request.params.arguments);
3401
- const projects = await listProjects(args);
3402
- return {
3403
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
3404
- };
3405
- }
3406
- case "list_project_members": {
3407
- const args = ListProjectMembersSchema.parse(request.params.arguments);
3408
- const { project_id, ...options } = args;
3409
- const members = await listProjectMembers(project_id, options);
3410
- return {
3411
- content: [{ type: "text", text: JSON.stringify(members, null, 2) }],
3412
- };
3413
- }
3414
- case "get_users": {
3415
- const args = GetUsersSchema.parse(request.params.arguments);
3416
- const usersMap = await getUsers(args.usernames);
3417
- return {
3418
- content: [{ type: "text", text: JSON.stringify(usersMap, null, 2) }],
3419
- };
3420
- }
3421
- case "create_note": {
3422
- const args = CreateNoteSchema.parse(request.params.arguments);
3423
- const { project_id, noteable_type, noteable_iid, body } = args;
3424
- const note = await createNote(project_id, noteable_type, noteable_iid, body);
3425
- return {
3426
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
3427
- };
3428
- }
3429
- case "get_draft_note": {
3430
- const args = GetDraftNoteSchema.parse(request.params.arguments);
3431
- const { project_id, merge_request_iid, draft_note_id } = args;
3432
- const draftNote = await getDraftNote(project_id, merge_request_iid, draft_note_id);
3433
- return {
3434
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
3435
- };
3436
- }
3437
- case "list_draft_notes": {
3438
- const args = ListDraftNotesSchema.parse(request.params.arguments);
3439
- const { project_id, merge_request_iid } = args;
3440
- const draftNotes = await listDraftNotes(project_id, merge_request_iid);
3441
- return {
3442
- content: [{ type: "text", text: JSON.stringify(draftNotes, null, 2) }],
3443
- };
3444
- }
3445
- case "create_draft_note": {
3446
- const args = CreateDraftNoteSchema.parse(request.params.arguments);
3447
- const { project_id, merge_request_iid, body, position, resolve_discussion } = args;
3448
- const draftNote = await createDraftNote(project_id, merge_request_iid, body, position, resolve_discussion);
3449
- return {
3450
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
3451
- };
3452
- }
3453
- case "update_draft_note": {
3454
- const args = UpdateDraftNoteSchema.parse(request.params.arguments);
3455
- const { project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion } = args;
3456
- const draftNote = await updateDraftNote(project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion);
3457
- return {
3458
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
3459
- };
3460
- }
3461
- case "delete_draft_note": {
3462
- const args = DeleteDraftNoteSchema.parse(request.params.arguments);
3463
- const { project_id, merge_request_iid, draft_note_id } = args;
3464
- await deleteDraftNote(project_id, merge_request_iid, draft_note_id);
3465
- return {
3466
- content: [{ type: "text", text: "Draft note deleted successfully" }],
3467
- };
3468
- }
3469
- case "publish_draft_note": {
3470
- const args = PublishDraftNoteSchema.parse(request.params.arguments);
3471
- const { project_id, merge_request_iid, draft_note_id } = args;
3472
- const publishedNote = await publishDraftNote(project_id, merge_request_iid, draft_note_id);
3473
- return {
3474
- content: [{ type: "text", text: JSON.stringify(publishedNote, null, 2) }],
3475
- };
3476
- }
3477
- case "bulk_publish_draft_notes": {
3478
- const args = BulkPublishDraftNotesSchema.parse(request.params.arguments);
3479
- const { project_id, merge_request_iid } = args;
3480
- const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid);
3481
- return {
3482
- content: [{ type: "text", text: JSON.stringify(publishedNotes, null, 2) }],
3483
- };
3484
- }
3485
- case "create_merge_request_thread": {
3486
- const args = CreateMergeRequestThreadSchema.parse(request.params.arguments);
3487
- const { project_id, merge_request_iid, body, position, created_at } = args;
3488
- const thread = await createMergeRequestThread(project_id, merge_request_iid, body, position, created_at);
3489
- return {
3490
- content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
3491
- };
3492
- }
3493
- case "list_issues": {
3494
- const args = ListIssuesSchema.parse(request.params.arguments);
3495
- const { project_id, ...options } = args;
3496
- const issues = await listIssues(project_id, options);
3497
- return {
3498
- content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
3499
- };
3500
- }
3501
- case "my_issues": {
3502
- const args = MyIssuesSchema.parse(request.params.arguments);
3503
- const issues = await myIssues(args);
3504
- return {
3505
- content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
3506
- };
3507
- }
3508
- case "get_issue": {
3509
- const args = GetIssueSchema.parse(request.params.arguments);
3510
- const issue = await getIssue(args.project_id, args.issue_iid);
3511
- return {
3512
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
3513
- };
3514
- }
3515
- case "update_issue": {
3516
- const args = UpdateIssueSchema.parse(request.params.arguments);
3517
- const { project_id, issue_iid, ...options } = args;
3518
- const issue = await updateIssue(project_id, issue_iid, options);
3519
- return {
3520
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
3521
- };
3522
- }
3523
- case "delete_issue": {
3524
- const args = DeleteIssueSchema.parse(request.params.arguments);
3525
- await deleteIssue(args.project_id, args.issue_iid);
3526
- return {
3527
- content: [
3528
- {
3529
- type: "text",
3530
- text: JSON.stringify({ status: "success", message: "Issue deleted successfully" }, null, 2),
3531
- },
3532
- ],
3533
- };
3534
- }
3535
- case "list_issue_links": {
3536
- const args = ListIssueLinksSchema.parse(request.params.arguments);
3537
- const links = await listIssueLinks(args.project_id, args.issue_iid);
3538
- return {
3539
- content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
3540
- };
3541
- }
3542
- case "list_issue_discussions": {
3543
- const args = ListIssueDiscussionsSchema.parse(request.params.arguments);
3544
- const { project_id, issue_iid, ...options } = args;
3545
- const discussions = await listIssueDiscussions(project_id, issue_iid, options);
3546
- return {
3547
- content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
3548
- };
3549
- }
3550
- case "get_issue_link": {
3551
- const args = GetIssueLinkSchema.parse(request.params.arguments);
3552
- const link = await getIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
3553
- return {
3554
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
3555
- };
3556
- }
3557
- case "create_issue_link": {
3558
- const args = CreateIssueLinkSchema.parse(request.params.arguments);
3559
- const link = await createIssueLink(args.project_id, args.issue_iid, args.target_project_id, args.target_issue_iid, args.link_type);
3560
- return {
3561
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
3562
- };
3563
- }
3564
- case "delete_issue_link": {
3565
- const args = DeleteIssueLinkSchema.parse(request.params.arguments);
3566
- await deleteIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
3567
- return {
3568
- content: [
3569
- {
3570
- type: "text",
3571
- text: JSON.stringify({
3572
- status: "success",
3573
- message: "Issue link deleted successfully",
3574
- }, null, 2),
3575
- },
3576
- ],
3577
- };
3578
- }
3579
- case "list_labels": {
3580
- const args = ListLabelsSchema.parse(request.params.arguments);
3581
- const labels = await listLabels(args.project_id, args);
3582
- return {
3583
- content: [{ type: "text", text: JSON.stringify(labels, null, 2) }],
3584
- };
3585
- }
3586
- case "get_label": {
3587
- const args = GetLabelSchema.parse(request.params.arguments);
3588
- const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups);
3589
- return {
3590
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
3591
- };
3592
- }
3593
- case "create_label": {
3594
- const args = CreateLabelSchema.parse(request.params.arguments);
3595
- const label = await createLabel(args.project_id, args);
3596
- return {
3597
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
3598
- };
3599
- }
3600
- case "update_label": {
3601
- const args = UpdateLabelSchema.parse(request.params.arguments);
3602
- const { project_id, label_id, ...options } = args;
3603
- const label = await updateLabel(project_id, label_id, options);
3604
- return {
3605
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
3606
- };
92
+ if (!verified) {
93
+ res.status(401).send("Token does not match session");
94
+ return;
95
+ }
96
+ logger.debug({
97
+ tokenHash: tokenHash.slice(-8),
98
+ }, "auth token verified");
3607
99
  }
3608
- case "delete_label": {
3609
- const args = DeleteLabelSchema.parse(request.params.arguments);
3610
- await deleteLabel(args.project_id, args.label_id);
3611
- return {
3612
- content: [
3613
- {
3614
- type: "text",
3615
- text: JSON.stringify({ status: "success", message: "Label deleted successfully" }, null, 2),
3616
- },
3617
- ],
3618
- };
100
+ if (transport) {
101
+ try {
102
+ await transport.handlePostMessage(req, res);
103
+ }
104
+ catch (e) {
105
+ logger.error({ e }, "Transport error handling message");
106
+ res.status(500).send("Internal server error");
107
+ }
3619
108
  }
3620
- case "list_group_projects": {
3621
- const args = ListGroupProjectsSchema.parse(request.params.arguments);
3622
- const projects = await listGroupProjects(args);
3623
- return {
3624
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
3625
- };
109
+ });
110
+ }
111
+ if (streamableHttpEnabled) {
112
+ const transports = {};
113
+ // Streamable HTTP endpoint - handles both session creation and message handling
114
+ app.post('/mcp', authMiddleware, async (req, res) => {
115
+ const sessionId = req.headers['mcp-session-id'];
116
+ try {
117
+ let transport;
118
+ if (sessionId && transports[sessionId]) {
119
+ // Reuse existing transport for ongoing session
120
+ const session = transports[sessionId];
121
+ if (session.tokenHash) {
122
+ if (!req.auth) {
123
+ res.status(401).send("No authorization information sent");
124
+ return;
125
+ }
126
+ const gitlabToken = req.auth.token;
127
+ if (!gitlabToken) {
128
+ res.status(401).send("No valid token info found in request");
129
+ return;
130
+ }
131
+ const verified = await argon2.verify(session.tokenHash, gitlabToken, {
132
+ salt: argon2Salt,
133
+ });
134
+ if (!verified) {
135
+ res.status(401).send("Token does not match session");
136
+ return;
137
+ }
138
+ logger.debug({
139
+ tokenHash: session.tokenHash.slice(-8),
140
+ }, "auth token verified");
141
+ }
142
+ transport = session.transport;
143
+ try {
144
+ await transport.handleRequest(req, res, req.body);
145
+ }
146
+ catch (e) {
147
+ logger.error("Transport error handling request:", e);
148
+ res.status(500).send("Internal server error");
149
+ return;
150
+ }
151
+ }
152
+ else {
153
+ // Create new transport for new session
154
+ transport = new StreamableHTTPServerTransport({
155
+ sessionIdGenerator: () => randomUUID(),
156
+ onsessioninitialized: (newSessionId) => {
157
+ transports[newSessionId] = {
158
+ transport,
159
+ };
160
+ // if we have a valid auth info here, either obtained from the passthrough token or oauth, we tie it to a session.
161
+ if (req.auth) {
162
+ transports[newSessionId].tokenHash = argon2.hashSync(req.auth.token, {
163
+ salt: argon2Salt,
164
+ });
165
+ logger.debug({
166
+ tokenHash: transports[newSessionId].tokenHash.slice(-8),
167
+ }, "auth session created for token");
168
+ }
169
+ logger.warn(`Streamable HTTP session initialized: ${newSessionId}`);
170
+ }
171
+ });
172
+ // Set up cleanup handler when transport closes
173
+ transport.onclose = () => {
174
+ const sid = transport.sessionId;
175
+ if (sid && transports[sid]) {
176
+ logger.warn(`Streamable HTTP transport closed for session ${sid}, cleaning up`);
177
+ delete transports[sid];
178
+ }
179
+ };
180
+ // Connect transport to MCP server before handling the request
181
+ try {
182
+ await mcpserver.connect(transport);
183
+ }
184
+ catch (e) {
185
+ logger.error({ e }, "Transport error connecting to MCP server:");
186
+ res.status(500).send("Internal server error");
187
+ return;
188
+ }
189
+ try {
190
+ await transport.handleRequest(req, res, req.body);
191
+ }
192
+ catch (e) {
193
+ logger.error({ e }, "Transport error handling request:");
194
+ res.status(500).send("Internal server error");
195
+ return;
196
+ }
197
+ }
3626
198
  }
3627
- case "list_wiki_pages": {
3628
- const { project_id, page, per_page, with_content } = ListWikiPagesSchema.parse(request.params.arguments);
3629
- const wikiPages = await listWikiPages(project_id, {
3630
- page,
3631
- per_page,
3632
- with_content,
199
+ catch (error) {
200
+ logger.error('Streamable HTTP error:', error);
201
+ res.status(500).json({
202
+ error: 'Internal server error',
203
+ message: error instanceof Error ? error.message : 'Unknown error'
3633
204
  });
3634
- return {
3635
- content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
3636
- };
3637
- }
3638
- case "get_wiki_page": {
3639
- const { project_id, slug } = GetWikiPageSchema.parse(request.params.arguments);
3640
- const wikiPage = await getWikiPage(project_id, slug);
3641
- return {
3642
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
3643
- };
3644
- }
3645
- case "create_wiki_page": {
3646
- const { project_id, title, content, format } = CreateWikiPageSchema.parse(request.params.arguments);
3647
- const wikiPage = await createWikiPage(project_id, title, content, format);
3648
- return {
3649
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
3650
- };
3651
- }
3652
- case "update_wiki_page": {
3653
- const { project_id, slug, title, content, format } = UpdateWikiPageSchema.parse(request.params.arguments);
3654
- const wikiPage = await updateWikiPage(project_id, slug, title, content, format);
3655
- return {
3656
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
3657
- };
3658
- }
3659
- case "delete_wiki_page": {
3660
- const { project_id, slug } = DeleteWikiPageSchema.parse(request.params.arguments);
3661
- await deleteWikiPage(project_id, slug);
3662
- return {
3663
- content: [
3664
- {
3665
- type: "text",
3666
- text: JSON.stringify({
3667
- status: "success",
3668
- message: "Wiki page deleted successfully",
3669
- }, null, 2),
3670
- },
3671
- ],
3672
- };
3673
- }
3674
- case "get_repository_tree": {
3675
- const args = GetRepositoryTreeSchema.parse(request.params.arguments);
3676
- const tree = await getRepositoryTree(args);
3677
- return {
3678
- content: [{ type: "text", text: JSON.stringify(tree, null, 2) }],
3679
- };
3680
- }
3681
- case "list_pipelines": {
3682
- const args = ListPipelinesSchema.parse(request.params.arguments);
3683
- const { project_id, ...options } = args;
3684
- const pipelines = await listPipelines(project_id, options);
3685
- return {
3686
- content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }],
3687
- };
3688
- }
3689
- case "get_pipeline": {
3690
- const { project_id, pipeline_id } = GetPipelineSchema.parse(request.params.arguments);
3691
- const pipeline = await getPipeline(project_id, pipeline_id);
3692
- return {
3693
- content: [
3694
- {
3695
- type: "text",
3696
- text: JSON.stringify(pipeline, null, 2),
3697
- },
3698
- ],
3699
- };
3700
- }
3701
- case "list_pipeline_jobs": {
3702
- const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(request.params.arguments);
3703
- const jobs = await listPipelineJobs(project_id, pipeline_id, options);
3704
- return {
3705
- content: [
3706
- {
3707
- type: "text",
3708
- text: JSON.stringify(jobs, null, 2),
3709
- },
3710
- ],
3711
- };
3712
- }
3713
- case "list_pipeline_trigger_jobs": {
3714
- const { project_id, pipeline_id, ...options } = ListPipelineTriggerJobsSchema.parse(request.params.arguments);
3715
- const triggerJobs = await listPipelineTriggerJobs(project_id, pipeline_id, options);
3716
- return {
3717
- content: [
3718
- {
3719
- type: "text",
3720
- text: JSON.stringify(triggerJobs, null, 2),
3721
- },
3722
- ],
3723
- };
3724
- }
3725
- case "get_pipeline_job": {
3726
- const { project_id, job_id } = GetPipelineJobOutputSchema.parse(request.params.arguments);
3727
- const jobDetails = await getPipelineJob(project_id, job_id);
3728
- return {
3729
- content: [
3730
- {
3731
- type: "text",
3732
- text: JSON.stringify(jobDetails, null, 2),
3733
- },
3734
- ],
3735
- };
3736
- }
3737
- case "get_pipeline_job_output": {
3738
- const { project_id, job_id, limit, offset } = GetPipelineJobOutputSchema.parse(request.params.arguments);
3739
- const jobOutput = await getPipelineJobOutput(project_id, job_id, limit, offset);
3740
- return {
3741
- content: [
3742
- {
3743
- type: "text",
3744
- text: jobOutput,
3745
- },
3746
- ],
3747
- };
3748
- }
3749
- case "create_pipeline": {
3750
- const { project_id, ref, variables } = CreatePipelineSchema.parse(request.params.arguments);
3751
- const pipeline = await createPipeline(project_id, ref, variables);
3752
- return {
3753
- content: [
3754
- {
3755
- type: "text",
3756
- text: `Created pipeline #${pipeline.id} for ${ref}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
3757
- },
3758
- ],
3759
- };
3760
- }
3761
- case "retry_pipeline": {
3762
- const { project_id, pipeline_id } = RetryPipelineSchema.parse(request.params.arguments);
3763
- const pipeline = await retryPipeline(project_id, pipeline_id);
3764
- return {
3765
- content: [
3766
- {
3767
- type: "text",
3768
- text: `Retried pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
3769
- },
3770
- ],
3771
- };
3772
205
  }
3773
- case "cancel_pipeline": {
3774
- const { project_id, pipeline_id } = CancelPipelineSchema.parse(request.params.arguments);
3775
- const pipeline = await cancelPipeline(project_id, pipeline_id);
3776
- return {
3777
- content: [
3778
- {
3779
- type: "text",
3780
- text: `Canceled pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`,
3781
- },
3782
- ],
3783
- };
3784
- }
3785
- case "list_merge_requests": {
3786
- const args = ListMergeRequestsSchema.parse(request.params.arguments);
3787
- const mergeRequests = await listMergeRequests(args.project_id, args);
3788
- return {
3789
- content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }],
3790
- };
3791
- }
3792
- case "list_milestones": {
3793
- const { project_id, ...options } = ListProjectMilestonesSchema.parse(request.params.arguments);
3794
- const milestones = await listProjectMilestones(project_id, options);
3795
- return {
3796
- content: [
3797
- {
3798
- type: "text",
3799
- text: JSON.stringify(milestones, null, 2),
3800
- },
3801
- ],
3802
- };
3803
- }
3804
- case "get_milestone": {
3805
- const { project_id, milestone_id } = GetProjectMilestoneSchema.parse(request.params.arguments);
3806
- const milestone = await getProjectMilestone(project_id, milestone_id);
3807
- return {
3808
- content: [
3809
- {
3810
- type: "text",
3811
- text: JSON.stringify(milestone, null, 2),
3812
- },
3813
- ],
3814
- };
3815
- }
3816
- case "create_milestone": {
3817
- const { project_id, ...options } = CreateProjectMilestoneSchema.parse(request.params.arguments);
3818
- const milestone = await createProjectMilestone(project_id, options);
3819
- return {
3820
- content: [
3821
- {
3822
- type: "text",
3823
- text: JSON.stringify(milestone, null, 2),
3824
- },
3825
- ],
3826
- };
3827
- }
3828
- case "edit_milestone": {
3829
- const { project_id, milestone_id, ...options } = EditProjectMilestoneSchema.parse(request.params.arguments);
3830
- const milestone = await editProjectMilestone(project_id, milestone_id, options);
3831
- return {
3832
- content: [
3833
- {
3834
- type: "text",
3835
- text: JSON.stringify(milestone, null, 2),
3836
- },
3837
- ],
3838
- };
3839
- }
3840
- case "delete_milestone": {
3841
- const { project_id, milestone_id } = DeleteProjectMilestoneSchema.parse(request.params.arguments);
3842
- await deleteProjectMilestone(project_id, milestone_id);
3843
- return {
3844
- content: [
3845
- {
3846
- type: "text",
3847
- text: JSON.stringify({
3848
- status: "success",
3849
- message: "Milestone deleted successfully",
3850
- }, null, 2),
3851
- },
3852
- ],
3853
- };
3854
- }
3855
- case "get_milestone_issue": {
3856
- const { project_id, milestone_id } = GetMilestoneIssuesSchema.parse(request.params.arguments);
3857
- const issues = await getMilestoneIssues(project_id, milestone_id);
3858
- return {
3859
- content: [
3860
- {
3861
- type: "text",
3862
- text: JSON.stringify(issues, null, 2),
3863
- },
3864
- ],
3865
- };
3866
- }
3867
- case "get_milestone_merge_requests": {
3868
- const { project_id, milestone_id } = GetMilestoneMergeRequestsSchema.parse(request.params.arguments);
3869
- const mergeRequests = await getMilestoneMergeRequests(project_id, milestone_id);
3870
- return {
3871
- content: [
3872
- {
3873
- type: "text",
3874
- text: JSON.stringify(mergeRequests, null, 2),
3875
- },
3876
- ],
3877
- };
3878
- }
3879
- case "promote_milestone": {
3880
- const { project_id, milestone_id } = PromoteProjectMilestoneSchema.parse(request.params.arguments);
3881
- const milestone = await promoteProjectMilestone(project_id, milestone_id);
3882
- return {
3883
- content: [
3884
- {
3885
- type: "text",
3886
- text: JSON.stringify(milestone, null, 2),
3887
- },
3888
- ],
3889
- };
3890
- }
3891
- case "get_milestone_burndown_events": {
3892
- const { project_id, milestone_id } = GetMilestoneBurndownEventsSchema.parse(request.params.arguments);
3893
- const events = await getMilestoneBurndownEvents(project_id, milestone_id);
3894
- return {
3895
- content: [
3896
- {
3897
- type: "text",
3898
- text: JSON.stringify(events, null, 2),
3899
- },
3900
- ],
3901
- };
3902
- }
3903
- case "list_commits": {
3904
- const args = ListCommitsSchema.parse(request.params.arguments);
3905
- const commits = await listCommits(args.project_id, args);
3906
- return {
3907
- content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
3908
- };
3909
- }
3910
- case "get_commit": {
3911
- const args = GetCommitSchema.parse(request.params.arguments);
3912
- const commit = await getCommit(args.project_id, args.sha, args.stats);
3913
- return {
3914
- content: [{ type: "text", text: JSON.stringify(commit, null, 2) }],
3915
- };
3916
- }
3917
- case "get_commit_diff": {
3918
- const args = GetCommitDiffSchema.parse(request.params.arguments);
3919
- const diff = await getCommitDiff(args.project_id, args.sha);
3920
- return {
3921
- content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
3922
- };
3923
- }
3924
- case "list_group_iterations": {
3925
- const args = ListGroupIterationsSchema.parse(request.params.arguments);
3926
- const iterations = await listGroupIterations(args.group_id, args);
3927
- return {
3928
- content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
3929
- };
3930
- }
3931
- case "upload_markdown": {
3932
- const args = MarkdownUploadSchema.parse(request.params.arguments);
3933
- const upload = await markdownUpload(args.project_id, args.file_path);
3934
- return {
3935
- content: [{ type: "text", text: JSON.stringify(upload, null, 2) }],
3936
- };
3937
- }
3938
- case "download_attachment": {
3939
- const args = DownloadAttachmentSchema.parse(request.params.arguments);
3940
- const filePath = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
3941
- return {
3942
- content: [{ type: "text", text: JSON.stringify({ success: true, file_path: filePath }, null, 2) }],
3943
- };
3944
- }
3945
- default:
3946
- throw new Error(`Unknown tool: ${request.params.name}`);
3947
- }
3948
- }
3949
- catch (error) {
3950
- logger.debug(request.params);
3951
- if (error instanceof z.ZodError) {
3952
- throw new Error(`Invalid arguments: ${error.errors
3953
- .map(e => `${e.path.join(".")}: ${e.message}`)
3954
- .join(", ")}`);
3955
- }
3956
- throw error;
3957
- }
3958
- });
3959
- /**
3960
- * Color constants for terminal output
3961
- */
3962
- const colorGreen = "\x1b[32m";
3963
- const colorReset = "\x1b[0m";
3964
- /**
3965
- * Determine the transport mode based on environment variables and availability
3966
- *
3967
- * Transport mode priority (highest to lowest):
3968
- * 1. STREAMABLE_HTTP
3969
- * 2. SSE
3970
- * 3. STDIO
3971
- */
3972
- function determineTransportMode() {
3973
- // Check for streamable-http support (highest priority)
3974
- if (STREAMABLE_HTTP) {
3975
- return TransportMode.STREAMABLE_HTTP;
3976
- }
3977
- // Check for SSE support (medium priority)
3978
- if (SSE) {
3979
- return TransportMode.SSE;
3980
- }
3981
- // Default to stdio (lowest priority)
3982
- return TransportMode.STDIO;
3983
- }
3984
- /**
3985
- * Start server with stdio transport
3986
- */
3987
- async function startStdioServer() {
3988
- const transport = new StdioServerTransport();
3989
- await server.connect(transport);
3990
- }
3991
- /**
3992
- * Start server with traditional SSE transport
3993
- */
3994
- async function startSSEServer() {
3995
- const app = express();
3996
- const transports = {};
3997
- app.get("/sse", async (_, res) => {
3998
- const transport = new SSEServerTransport("/messages", res);
3999
- transports[transport.sessionId] = transport;
4000
- res.on("close", () => {
4001
- delete transports[transport.sessionId];
4002
206
  });
4003
- await server.connect(transport);
4004
- });
4005
- app.post("/messages", async (req, res) => {
4006
- const sessionId = req.query.sessionId;
4007
- const transport = transports[sessionId];
4008
- if (transport) {
4009
- await transport.handlePostMessage(req, res);
4010
- }
4011
- else {
4012
- res.status(400).send("No transport found for sessionId");
4013
- }
4014
- });
207
+ }
4015
208
  app.get("/health", (_, res) => {
4016
209
  res.status(200).json({
4017
210
  status: "healthy",
4018
- version: SERVER_VERSION,
4019
- transport: TransportMode.SSE,
211
+ version: process.env.npm_package_version || "unknown",
4020
212
  });
4021
213
  });
4022
- app.listen(Number(PORT), HOST, () => {
4023
- logger.info(`GitLab MCP Server running with SSE transport`);
214
+ app.listen(Number(config.PORT), config.HOST, () => {
215
+ const enabledModes = [];
216
+ if (sseEnabled)
217
+ enabledModes.push('SSE');
218
+ if (streamableHttpEnabled)
219
+ enabledModes.push('Streamable HTTP');
220
+ logger.info(`GitLab MCP Server running with ${enabledModes.join(' and ')} transport(s)`);
4024
221
  const colorGreen = "\x1b[32m";
4025
222
  const colorReset = "\x1b[0m";
4026
- logger.info(`${colorGreen}Endpoint: http://${HOST}:${PORT}/sse${colorReset}`);
4027
- });
4028
- }
4029
- /**
4030
- * Start server with Streamable HTTP transport
4031
- */
4032
- async function startStreamableHTTPServer() {
4033
- const app = express();
4034
- const streamableTransports = {};
4035
- // Configure Express middleware
4036
- app.use(express.json());
4037
- // Streamable HTTP endpoint - handles both session creation and message handling
4038
- app.post("/mcp", async (req, res) => {
4039
- const sessionId = req.headers["mcp-session-id"];
4040
- try {
4041
- let transport;
4042
- if (sessionId && streamableTransports[sessionId]) {
4043
- // Reuse existing transport for ongoing session
4044
- transport = streamableTransports[sessionId];
4045
- await transport.handleRequest(req, res, req.body);
4046
- }
4047
- else {
4048
- // Create new transport for new session
4049
- transport = new StreamableHTTPServerTransport({
4050
- sessionIdGenerator: () => randomUUID(),
4051
- onsessioninitialized: (newSessionId) => {
4052
- streamableTransports[newSessionId] = transport;
4053
- logger.warn(`Streamable HTTP session initialized: ${newSessionId}`);
4054
- },
4055
- });
4056
- // Set up cleanup handler when transport closes
4057
- transport.onclose = () => {
4058
- const sid = transport.sessionId;
4059
- if (sid && streamableTransports[sid]) {
4060
- logger.warn(`Streamable HTTP transport closed for session ${sid}, cleaning up`);
4061
- delete streamableTransports[sid];
4062
- }
4063
- };
4064
- // Connect transport to MCP server before handling the request
4065
- await server.connect(transport);
4066
- await transport.handleRequest(req, res, req.body);
4067
- }
223
+ if (sseEnabled) {
224
+ logger.info(`${colorGreen}SSE Endpoint: http://${config.HOST}:${config.PORT}/sse${colorReset}`);
4068
225
  }
4069
- catch (error) {
4070
- logger.error("Streamable HTTP error:", error);
4071
- res.status(500).json({
4072
- error: "Internal server error",
4073
- message: error instanceof Error ? error.message : "Unknown error",
4074
- });
226
+ if (streamableHttpEnabled) {
227
+ logger.info(`${colorGreen}Streamable HTTP Endpoint: http://${config.HOST}:${config.PORT}/mcp${colorReset}`);
4075
228
  }
4076
229
  });
4077
- // Health check endpoint
4078
- app.get("/health", (_, res) => {
4079
- res.status(200).json({
4080
- status: "healthy",
4081
- version: SERVER_VERSION,
4082
- transport: TransportMode.STREAMABLE_HTTP,
4083
- activeSessions: Object.keys(streamableTransports).length,
4084
- });
4085
- });
4086
- // Start server
4087
- app.listen(Number(PORT), HOST, () => {
4088
- logger.info(`GitLab MCP Server running with Streamable HTTP transport`);
4089
- logger.info(`${colorGreen}Endpoint: http://${HOST}:${PORT}/mcp${colorReset}`);
4090
- });
4091
230
  }
4092
231
  /**
4093
- * Initialize server with specific transport mode
4094
- * Handle transport-specific initialization logic
232
+ * Initialize server based on enabled transport modes
4095
233
  */
4096
- async function initializeServerByTransportMode(mode) {
4097
- logger.info("Initializing server with transport mode:", mode);
4098
- switch (mode) {
4099
- case TransportMode.STDIO:
4100
- logger.warn("Starting GitLab MCP Server with stdio transport");
4101
- await startStdioServer();
4102
- break;
4103
- case TransportMode.SSE:
4104
- logger.warn("Starting GitLab MCP Server with SSE transport");
4105
- await startSSEServer();
4106
- break;
4107
- case TransportMode.STREAMABLE_HTTP:
4108
- logger.warn("Starting GitLab MCP Server with Streamable HTTP transport");
4109
- await startStreamableHTTPServer();
4110
- break;
4111
- default:
4112
- // This should never happen with proper enum usage, but TypeScript requires it
4113
- const exhaustiveCheck = mode;
4114
- throw new Error(`Unknown transport mode: ${exhaustiveCheck}`);
234
+ async function initializeServer(modes) {
235
+ if (modes.stdio) {
236
+ logger.warn('Starting GitLab MCP Server with stdio transport');
237
+ await startStdioServer();
238
+ }
239
+ else if (modes.sse || modes.streamableHttp) {
240
+ logger.warn('Starting GitLab MCP Server with HTTP transport(s)');
241
+ await startExpressServer({
242
+ sseEnabled: modes.sse,
243
+ streamableHttpEnabled: modes.streamableHttp
244
+ });
245
+ }
246
+ else {
247
+ throw new Error('No transport mode enabled');
4115
248
  }
4116
249
  }
4117
250
  /**
@@ -4120,15 +253,14 @@ async function initializeServerByTransportMode(mode) {
4120
253
  */
4121
254
  async function runServer() {
4122
255
  try {
4123
- const transportMode = determineTransportMode();
4124
- await initializeServerByTransportMode(transportMode);
256
+ const transportModes = determineTransportModes();
257
+ await initializeServer(transportModes);
4125
258
  }
4126
259
  catch (error) {
4127
260
  logger.error("Error initializing server:", error);
4128
261
  process.exit(1);
4129
262
  }
4130
263
  }
4131
- // 下記の2行を追記
4132
264
  runServer().catch(error => {
4133
265
  logger.error("Fatal error in main():", error);
4134
266
  process.exit(1);