@zereight/mcp-gitlab 1.0.75 → 1.0.77

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,40 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
3
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
6
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
- import nodeFetch from "node-fetch";
7
+ import express from "express";
8
8
  import fetchCookie from "fetch-cookie";
9
- import { CookieJar, parse as parseCookie } from "tough-cookie";
10
- import { SocksProxyAgent } from "socks-proxy-agent";
11
- import { HttpsProxyAgent } from "https-proxy-agent";
9
+ import fs from "fs";
12
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";
13
17
  import { z } from "zod";
14
18
  import { zodToJsonSchema } from "zod-to-json-schema";
15
- import { fileURLToPath } from "url";
16
- import { dirname } from "path";
17
- import fs from "fs";
18
- import path from "path";
19
- import express from "express";
20
19
  // Add type imports for proxy agents
21
20
  import { Agent } from "http";
22
21
  import { Agent as HttpsAgent } from "https";
23
22
  import { URL } from "url";
24
- import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, GitLabNamespaceSchema, GitLabNamespaceExistsResponseSchema, GitLabProjectSchema, GitLabUserSchema, GitLabUsersResponseSchema, GetUsersSchema, CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, GitLabDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, ListIssuesSchema, GetIssueSchema, UpdateIssueSchema, DeleteIssueSchema, GitLabIssueLinkSchema, GitLabIssueWithLinkDetailsSchema, ListIssueLinksSchema, ListIssueDiscussionsSchema, GetIssueLinkSchema, CreateIssueLinkSchema, DeleteIssueLinkSchema, ListNamespacesSchema, GetNamespaceSchema, VerifyNamespaceSchema, GetProjectSchema, ListProjectsSchema, ListLabelsSchema, GetLabelSchema, CreateLabelSchema, UpdateLabelSchema, DeleteLabelSchema, CreateNoteSchema, CreateMergeRequestThreadSchema, ListGroupProjectsSchema, ListWikiPagesSchema, GetWikiPageSchema, CreateWikiPageSchema, UpdateWikiPageSchema, DeleteWikiPageSchema, GitLabWikiPageSchema, GetRepositoryTreeSchema, GitLabTreeItemSchema, GitLabPipelineSchema, GetPipelineSchema, ListPipelinesSchema, ListPipelineJobsSchema, CreatePipelineSchema, RetryPipelineSchema, CancelPipelineSchema,
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
25
  // pipeline job schemas
26
- GetPipelineJobOutputSchema, GitLabPipelineJobSchema,
26
+ GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
27
27
  // Discussion Schemas
28
28
  GitLabDiscussionNoteSchema, // Added
29
- GitLabDiscussionSchema, PaginatedDiscussionsResponseSchema, UpdateMergeRequestNoteSchema, // Added
30
- CreateMergeRequestNoteSchema, // Added
31
- ListMergeRequestDiscussionsSchema, UpdateIssueNoteSchema, CreateIssueNoteSchema, ListMergeRequestsSchema, GitLabMilestonesSchema, ListProjectMilestonesSchema, GetProjectMilestoneSchema, CreateProjectMilestoneSchema, EditProjectMilestoneSchema, DeleteProjectMilestoneSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, PromoteProjectMilestoneSchema, GetMilestoneBurndownEventsSchema, GitLabCompareResultSchema, GetBranchDiffsSchema, ListCommitsSchema, GetCommitSchema, GetCommitDiffSchema, ListMergeRequestDiffsSchema, } from "./schemas.js";
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";
32
33
  import { randomUUID } from "crypto";
33
- import { pino } from 'pino';
34
+ import { pino } from "pino";
34
35
  const logger = pino({
35
- level: process.env.LOG_LEVEL || 'info',
36
+ level: process.env.LOG_LEVEL || "info",
36
37
  transport: {
37
- target: 'pino-pretty',
38
+ target: "pino-pretty",
38
39
  options: {
39
40
  colorize: true,
40
41
  levelFirst: true,
@@ -84,7 +85,7 @@ const USE_MILESTONE = process.env.USE_MILESTONE === "true";
84
85
  const USE_PIPELINE = process.env.USE_PIPELINE === "true";
85
86
  const SSE = process.env.SSE === "true";
86
87
  const STREAMABLE_HTTP = process.env.STREAMABLE_HTTP === "true";
87
- const HOST = process.env.HOST || '0.0.0.0';
88
+ const HOST = process.env.HOST || "0.0.0.0";
88
89
  const PORT = process.env.PORT || 3002;
89
90
  // Add proxy configuration
90
91
  const HTTP_PROXY = process.env.HTTP_PROXY;
@@ -172,13 +173,13 @@ async function ensureSessionForRequest() {
172
173
  const baseUrl = `${apiUrl.protocol}//${apiUrl.hostname}`;
173
174
  // Check if we already have GitLab session cookies
174
175
  const gitlabCookies = cookieJar.getCookiesSync(baseUrl);
175
- const hasSessionCookie = gitlabCookies.some(cookie => cookie.key === '_gitlab_session' || cookie.key === 'remember_user_token');
176
+ const hasSessionCookie = gitlabCookies.some(cookie => cookie.key === "_gitlab_session" || cookie.key === "remember_user_token");
176
177
  if (!hasSessionCookie) {
177
178
  try {
178
179
  // Establish session with a lightweight request
179
180
  await fetch(`${GITLAB_API_URL}/user`, {
180
181
  ...DEFAULT_FETCH_CONFIG,
181
- redirect: 'follow'
182
+ redirect: "follow",
182
183
  }).catch(() => {
183
184
  // Ignore errors - the important thing is that cookies get set during redirects
184
185
  });
@@ -213,6 +214,11 @@ const DEFAULT_FETCH_CONFIG = {
213
214
  };
214
215
  // Define all available tools
215
216
  const allTools = [
217
+ {
218
+ name: "merge_merge_request",
219
+ description: "Merge a merge request in a GitLab project",
220
+ inputSchema: zodToJsonSchema(MergeMergeRequestSchema),
221
+ },
216
222
  {
217
223
  name: "create_or_update_file",
218
224
  description: "Create or update a single file in a GitLab project",
@@ -308,6 +314,41 @@ const allTools = [
308
314
  description: "Add a new note to an existing merge request thread",
309
315
  inputSchema: zodToJsonSchema(CreateMergeRequestNoteSchema),
310
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
+ },
311
352
  {
312
353
  name: "update_issue_note",
313
354
  description: "Modify an existing issue thread note",
@@ -323,6 +364,11 @@ const allTools = [
323
364
  description: "List issues (default: created by current user only; use scope='all' for all accessible issues)",
324
365
  inputSchema: zodToJsonSchema(ListIssuesSchema),
325
366
  },
367
+ {
368
+ name: "my_issues",
369
+ description: "List issues assigned to the authenticated user (defaults to open issues)",
370
+ inputSchema: zodToJsonSchema(MyIssuesSchema),
371
+ },
326
372
  {
327
373
  name: "get_issue",
328
374
  description: "Get details of a specific issue in a GitLab project",
@@ -388,6 +434,11 @@ const allTools = [
388
434
  description: "List projects accessible by the current user",
389
435
  inputSchema: zodToJsonSchema(ListProjectsSchema),
390
436
  },
437
+ {
438
+ name: "list_project_members",
439
+ description: "List members of a GitLab project",
440
+ inputSchema: zodToJsonSchema(ListProjectMembersSchema),
441
+ },
391
442
  {
392
443
  name: "list_labels",
393
444
  description: "List labels for a project",
@@ -463,6 +514,11 @@ const allTools = [
463
514
  description: "List all jobs in a specific pipeline",
464
515
  inputSchema: zodToJsonSchema(ListPipelineJobsSchema),
465
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
+ },
466
522
  {
467
523
  name: "get_pipeline_job",
468
524
  description: "Get details of a GitLab pipeline job number",
@@ -558,6 +614,21 @@ const allTools = [
558
614
  description: "Get changes/diffs of a specific commit",
559
615
  inputSchema: zodToJsonSchema(GetCommitDiffSchema),
560
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
+ },
561
632
  ];
562
633
  // Define which tools are read-only
563
634
  const readOnlyTools = [
@@ -568,6 +639,7 @@ const readOnlyTools = [
568
639
  "get_branch_diffs",
569
640
  "mr_discussions",
570
641
  "list_issues",
642
+ "my_issues",
571
643
  "list_merge_requests",
572
644
  "get_issue",
573
645
  "list_issue_links",
@@ -577,12 +649,14 @@ const readOnlyTools = [
577
649
  "get_namespace",
578
650
  "verify_namespace",
579
651
  "get_project",
652
+ "list_projects",
653
+ "list_project_members",
580
654
  "get_pipeline",
581
655
  "list_pipelines",
582
656
  "list_pipeline_jobs",
657
+ "list_pipeline_trigger_jobs",
583
658
  "get_pipeline_job",
584
659
  "get_pipeline_job_output",
585
- "list_projects",
586
660
  "list_labels",
587
661
  "get_label",
588
662
  "list_group_projects",
@@ -598,6 +672,9 @@ const readOnlyTools = [
598
672
  "list_commits",
599
673
  "get_commit",
600
674
  "get_commit_diff",
675
+ "list_group_iterations",
676
+ "get_group_iteration",
677
+ "download_attachment",
601
678
  ];
602
679
  // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
603
680
  const wikiToolNames = [
@@ -625,6 +702,7 @@ const pipelineToolNames = [
625
702
  "list_pipelines",
626
703
  "get_pipeline",
627
704
  "list_pipeline_jobs",
705
+ "list_pipeline_trigger_jobs",
628
706
  "get_pipeline_job",
629
707
  "get_pipeline_job_output",
630
708
  "create_pipeline",
@@ -653,6 +731,7 @@ function normalizeGitLabApiUrl(url) {
653
731
  // Use the normalizeGitLabApiUrl function to handle various URL formats
654
732
  const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || "");
655
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) || [];
656
735
  if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
657
736
  logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
658
737
  process.exit(1);
@@ -682,8 +761,24 @@ async function handleGitLabError(response) {
682
761
  /**
683
762
  * @param {string} projectId - The project ID parameter passed to the function
684
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
685
765
  */
686
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
+ }
687
782
  return GITLAB_PROJECT_ID || projectId;
688
783
  }
689
784
  /**
@@ -1616,6 +1711,26 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
1616
1711
  await handleGitLabError(response);
1617
1712
  return GitLabMergeRequestSchema.parse(await response.json());
1618
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
+ }
1619
1734
  /**
1620
1735
  * Create a new note (comment) on an issue or merge request
1621
1736
  * 📦 새로운 함수: createNote - 이슈 또는 병합 요청에 노트(댓글)를 추가하는 함수
@@ -1644,6 +1759,208 @@ noteableIid, body) {
1644
1759
  }
1645
1760
  return await response.json();
1646
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
+ }
1647
1964
  /**
1648
1965
  * Create a new thread on a merge request
1649
1966
  * 📦 새로운 함수: createMergeRequestThread - 병합 요청에 새로운 스레드(토론)를 생성하는 함수
@@ -2123,6 +2440,38 @@ async function listPipelineJobs(projectId, pipelineId, options = {}) {
2123
2440
  const data = await response.json();
2124
2441
  return z.array(GitLabPipelineJobSchema).parse(data);
2125
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
+ }
2126
2475
  async function getPipelineJob(projectId, jobId) {
2127
2476
  projectId = decodeURIComponent(projectId); // Decode project ID
2128
2477
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}`);
@@ -2162,14 +2511,14 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
2162
2511
  const fullTrace = await response.text();
2163
2512
  // Apply client-side pagination to limit context window usage
2164
2513
  if (limit !== undefined || offset !== undefined) {
2165
- const lines = fullTrace.split('\n');
2514
+ const lines = fullTrace.split("\n");
2166
2515
  const startOffset = offset || 0;
2167
2516
  const maxLines = limit || 1000;
2168
2517
  // Return lines from the end, skipping offset lines and limiting to maxLines
2169
2518
  const startIndex = Math.max(0, lines.length - startOffset - maxLines);
2170
2519
  const endIndex = lines.length - startOffset;
2171
2520
  const selectedLines = lines.slice(startIndex, endIndex);
2172
- const result = selectedLines.join('\n');
2521
+ const result = selectedLines.join("\n");
2173
2522
  // Add metadata about truncation
2174
2523
  if (startIndex > 0 || endIndex < lines.length) {
2175
2524
  const totalLines = lines.length;
@@ -2580,6 +2929,175 @@ async function getCommitDiff(projectId, sha) {
2580
2929
  const data = await response.json();
2581
2930
  return z.array(GitLabDiffSchema).parse(data);
2582
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
+ }
2944
+ /**
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
2950
+ */
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,
2968
+ };
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
+ }
3043
+ /**
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
3049
+ */
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;
3100
+ }
2583
3101
  server.setRequestHandler(ListToolsRequestSchema, async () => {
2584
3102
  // Apply read-only filter first
2585
3103
  const tools0 = GITLAB_READ_ONLY_MODE
@@ -2798,6 +3316,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2798
3316
  content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
2799
3317
  };
2800
3318
  }
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
+ };
3326
+ }
2801
3327
  case "mr_discussions": {
2802
3328
  const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments);
2803
3329
  const { project_id, merge_request_iid, ...options } = args;
@@ -2877,6 +3403,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2877
3403
  content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
2878
3404
  };
2879
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
+ }
2880
3414
  case "get_users": {
2881
3415
  const args = GetUsersSchema.parse(request.params.arguments);
2882
3416
  const usersMap = await getUsers(args.usernames);
@@ -2892,6 +3426,62 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2892
3426
  content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
2893
3427
  };
2894
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
+ }
2895
3485
  case "create_merge_request_thread": {
2896
3486
  const args = CreateMergeRequestThreadSchema.parse(request.params.arguments);
2897
3487
  const { project_id, merge_request_iid, body, position, created_at } = args;
@@ -2908,6 +3498,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2908
3498
  content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
2909
3499
  };
2910
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
+ }
2911
3508
  case "get_issue": {
2912
3509
  const args = GetIssueSchema.parse(request.params.arguments);
2913
3510
  const issue = await getIssue(args.project_id, args.issue_iid);
@@ -3029,7 +3626,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3029
3626
  }
3030
3627
  case "list_wiki_pages": {
3031
3628
  const { project_id, page, per_page, with_content } = ListWikiPagesSchema.parse(request.params.arguments);
3032
- const wikiPages = await listWikiPages(project_id, { page, per_page, with_content });
3629
+ const wikiPages = await listWikiPages(project_id, {
3630
+ page,
3631
+ per_page,
3632
+ with_content,
3633
+ });
3033
3634
  return {
3034
3635
  content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
3035
3636
  };
@@ -3109,6 +3710,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3109
3710
  ],
3110
3711
  };
3111
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
+ }
3112
3725
  case "get_pipeline_job": {
3113
3726
  const { project_id, job_id } = GetPipelineJobOutputSchema.parse(request.params.arguments);
3114
3727
  const jobDetails = await getPipelineJob(project_id, job_id);
@@ -3308,6 +3921,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3308
3921
  content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
3309
3922
  };
3310
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
+ }
3311
3945
  default:
3312
3946
  throw new Error(`Unknown tool: ${request.params.name}`);
3313
3947
  }
@@ -3340,7 +3974,7 @@ function determineTransportMode() {
3340
3974
  if (STREAMABLE_HTTP) {
3341
3975
  return TransportMode.STREAMABLE_HTTP;
3342
3976
  }
3343
- // Check for SSE support (medium priority)
3977
+ // Check for SSE support (medium priority)
3344
3978
  if (SSE) {
3345
3979
  return TransportMode.SSE;
3346
3980
  }
@@ -3382,7 +4016,7 @@ async function startSSEServer() {
3382
4016
  res.status(200).json({
3383
4017
  status: "healthy",
3384
4018
  version: SERVER_VERSION,
3385
- transport: TransportMode.SSE
4019
+ transport: TransportMode.SSE,
3386
4020
  });
3387
4021
  });
3388
4022
  app.listen(Number(PORT), HOST, () => {
@@ -3401,8 +4035,8 @@ async function startStreamableHTTPServer() {
3401
4035
  // Configure Express middleware
3402
4036
  app.use(express.json());
3403
4037
  // Streamable HTTP endpoint - handles both session creation and message handling
3404
- app.post('/mcp', async (req, res) => {
3405
- const sessionId = req.headers['mcp-session-id'];
4038
+ app.post("/mcp", async (req, res) => {
4039
+ const sessionId = req.headers["mcp-session-id"];
3406
4040
  try {
3407
4041
  let transport;
3408
4042
  if (sessionId && streamableTransports[sessionId]) {
@@ -3417,7 +4051,7 @@ async function startStreamableHTTPServer() {
3417
4051
  onsessioninitialized: (newSessionId) => {
3418
4052
  streamableTransports[newSessionId] = transport;
3419
4053
  logger.warn(`Streamable HTTP session initialized: ${newSessionId}`);
3420
- }
4054
+ },
3421
4055
  });
3422
4056
  // Set up cleanup handler when transport closes
3423
4057
  transport.onclose = () => {
@@ -3433,10 +4067,10 @@ async function startStreamableHTTPServer() {
3433
4067
  }
3434
4068
  }
3435
4069
  catch (error) {
3436
- logger.error('Streamable HTTP error:', error);
4070
+ logger.error("Streamable HTTP error:", error);
3437
4071
  res.status(500).json({
3438
- error: 'Internal server error',
3439
- message: error instanceof Error ? error.message : 'Unknown error'
4072
+ error: "Internal server error",
4073
+ message: error instanceof Error ? error.message : "Unknown error",
3440
4074
  });
3441
4075
  }
3442
4076
  });
@@ -3446,7 +4080,7 @@ async function startStreamableHTTPServer() {
3446
4080
  status: "healthy",
3447
4081
  version: SERVER_VERSION,
3448
4082
  transport: TransportMode.STREAMABLE_HTTP,
3449
- activeSessions: Object.keys(streamableTransports).length
4083
+ activeSessions: Object.keys(streamableTransports).length,
3450
4084
  });
3451
4085
  });
3452
4086
  // Start server
@@ -3460,18 +4094,18 @@ async function startStreamableHTTPServer() {
3460
4094
  * Handle transport-specific initialization logic
3461
4095
  */
3462
4096
  async function initializeServerByTransportMode(mode) {
3463
- logger.info('Initializing server with transport mode:', mode);
4097
+ logger.info("Initializing server with transport mode:", mode);
3464
4098
  switch (mode) {
3465
4099
  case TransportMode.STDIO:
3466
- logger.warn('Starting GitLab MCP Server with stdio transport');
4100
+ logger.warn("Starting GitLab MCP Server with stdio transport");
3467
4101
  await startStdioServer();
3468
4102
  break;
3469
4103
  case TransportMode.SSE:
3470
- logger.warn('Starting GitLab MCP Server with SSE transport');
4104
+ logger.warn("Starting GitLab MCP Server with SSE transport");
3471
4105
  await startSSEServer();
3472
4106
  break;
3473
4107
  case TransportMode.STREAMABLE_HTTP:
3474
- logger.warn('Starting GitLab MCP Server with Streamable HTTP transport');
4108
+ logger.warn("Starting GitLab MCP Server with Streamable HTTP transport");
3475
4109
  await startStreamableHTTPServer();
3476
4110
  break;
3477
4111
  default:
@@ -3494,6 +4128,7 @@ async function runServer() {
3494
4128
  process.exit(1);
3495
4129
  }
3496
4130
  }
4131
+ // 下記の2行を追記
3497
4132
  runServer().catch(error => {
3498
4133
  logger.error("Fatal error in main():", error);
3499
4134
  process.exit(1);