@zereight/mcp-gitlab 1.0.76 → 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, ListGroupIterationsSchema, GroupIteration, } 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",
@@ -563,6 +619,16 @@ const allTools = [
563
619
  description: "List group iterations with filtering options",
564
620
  inputSchema: zodToJsonSchema(ListGroupIterationsSchema),
565
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
+ },
566
632
  ];
567
633
  // Define which tools are read-only
568
634
  const readOnlyTools = [
@@ -573,6 +639,7 @@ const readOnlyTools = [
573
639
  "get_branch_diffs",
574
640
  "mr_discussions",
575
641
  "list_issues",
642
+ "my_issues",
576
643
  "list_merge_requests",
577
644
  "get_issue",
578
645
  "list_issue_links",
@@ -582,12 +649,14 @@ const readOnlyTools = [
582
649
  "get_namespace",
583
650
  "verify_namespace",
584
651
  "get_project",
652
+ "list_projects",
653
+ "list_project_members",
585
654
  "get_pipeline",
586
655
  "list_pipelines",
587
656
  "list_pipeline_jobs",
657
+ "list_pipeline_trigger_jobs",
588
658
  "get_pipeline_job",
589
659
  "get_pipeline_job_output",
590
- "list_projects",
591
660
  "list_labels",
592
661
  "get_label",
593
662
  "list_group_projects",
@@ -605,6 +674,7 @@ const readOnlyTools = [
605
674
  "get_commit_diff",
606
675
  "list_group_iterations",
607
676
  "get_group_iteration",
677
+ "download_attachment",
608
678
  ];
609
679
  // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
610
680
  const wikiToolNames = [
@@ -632,6 +702,7 @@ const pipelineToolNames = [
632
702
  "list_pipelines",
633
703
  "get_pipeline",
634
704
  "list_pipeline_jobs",
705
+ "list_pipeline_trigger_jobs",
635
706
  "get_pipeline_job",
636
707
  "get_pipeline_job_output",
637
708
  "create_pipeline",
@@ -660,6 +731,7 @@ function normalizeGitLabApiUrl(url) {
660
731
  // Use the normalizeGitLabApiUrl function to handle various URL formats
661
732
  const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || "");
662
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) || [];
663
735
  if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
664
736
  logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
665
737
  process.exit(1);
@@ -689,8 +761,24 @@ async function handleGitLabError(response) {
689
761
  /**
690
762
  * @param {string} projectId - The project ID parameter passed to the function
691
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
692
765
  */
693
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
+ }
694
782
  return GITLAB_PROJECT_ID || projectId;
695
783
  }
696
784
  /**
@@ -1623,6 +1711,26 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
1623
1711
  await handleGitLabError(response);
1624
1712
  return GitLabMergeRequestSchema.parse(await response.json());
1625
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
+ }
1626
1734
  /**
1627
1735
  * Create a new note (comment) on an issue or merge request
1628
1736
  * 📦 새로운 함수: createNote - 이슈 또는 병합 요청에 노트(댓글)를 추가하는 함수
@@ -1651,6 +1759,208 @@ noteableIid, body) {
1651
1759
  }
1652
1760
  return await response.json();
1653
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
+ }
1654
1964
  /**
1655
1965
  * Create a new thread on a merge request
1656
1966
  * 📦 새로운 함수: createMergeRequestThread - 병합 요청에 새로운 스레드(토론)를 생성하는 함수
@@ -2130,6 +2440,38 @@ async function listPipelineJobs(projectId, pipelineId, options = {}) {
2130
2440
  const data = await response.json();
2131
2441
  return z.array(GitLabPipelineJobSchema).parse(data);
2132
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
+ }
2133
2475
  async function getPipelineJob(projectId, jobId) {
2134
2476
  projectId = decodeURIComponent(projectId); // Decode project ID
2135
2477
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}`);
@@ -2169,14 +2511,14 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
2169
2511
  const fullTrace = await response.text();
2170
2512
  // Apply client-side pagination to limit context window usage
2171
2513
  if (limit !== undefined || offset !== undefined) {
2172
- const lines = fullTrace.split('\n');
2514
+ const lines = fullTrace.split("\n");
2173
2515
  const startOffset = offset || 0;
2174
2516
  const maxLines = limit || 1000;
2175
2517
  // Return lines from the end, skipping offset lines and limiting to maxLines
2176
2518
  const startIndex = Math.max(0, lines.length - startOffset - maxLines);
2177
2519
  const endIndex = lines.length - startOffset;
2178
2520
  const selectedLines = lines.slice(startIndex, endIndex);
2179
- const result = selectedLines.join('\n');
2521
+ const result = selectedLines.join("\n");
2180
2522
  // Add metadata about truncation
2181
2523
  if (startIndex > 0 || endIndex < lines.length) {
2182
2524
  const totalLines = lines.length;
@@ -2587,6 +2929,81 @@ async function getCommitDiff(projectId, sha) {
2587
2929
  const data = await response.json();
2588
2930
  return z.array(GitLabDiffSchema).parse(data);
2589
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
+ }
2590
3007
  /**
2591
3008
  * list group iterations
2592
3009
  *
@@ -2623,6 +3040,64 @@ async function listGroupIterations(groupId, options = {}) {
2623
3040
  const data = await response.json();
2624
3041
  return z.array(GroupIteration).parse(data);
2625
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
+ }
2626
3101
  server.setRequestHandler(ListToolsRequestSchema, async () => {
2627
3102
  // Apply read-only filter first
2628
3103
  const tools0 = GITLAB_READ_ONLY_MODE
@@ -2841,6 +3316,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2841
3316
  content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
2842
3317
  };
2843
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
+ }
2844
3327
  case "mr_discussions": {
2845
3328
  const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments);
2846
3329
  const { project_id, merge_request_iid, ...options } = args;
@@ -2920,6 +3403,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2920
3403
  content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
2921
3404
  };
2922
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
+ }
2923
3414
  case "get_users": {
2924
3415
  const args = GetUsersSchema.parse(request.params.arguments);
2925
3416
  const usersMap = await getUsers(args.usernames);
@@ -2935,6 +3426,62 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2935
3426
  content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
2936
3427
  };
2937
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
+ }
2938
3485
  case "create_merge_request_thread": {
2939
3486
  const args = CreateMergeRequestThreadSchema.parse(request.params.arguments);
2940
3487
  const { project_id, merge_request_iid, body, position, created_at } = args;
@@ -2951,6 +3498,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2951
3498
  content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
2952
3499
  };
2953
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
+ }
2954
3508
  case "get_issue": {
2955
3509
  const args = GetIssueSchema.parse(request.params.arguments);
2956
3510
  const issue = await getIssue(args.project_id, args.issue_iid);
@@ -3072,7 +3626,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3072
3626
  }
3073
3627
  case "list_wiki_pages": {
3074
3628
  const { project_id, page, per_page, with_content } = ListWikiPagesSchema.parse(request.params.arguments);
3075
- 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
+ });
3076
3634
  return {
3077
3635
  content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
3078
3636
  };
@@ -3152,6 +3710,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3152
3710
  ],
3153
3711
  };
3154
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
+ }
3155
3725
  case "get_pipeline_job": {
3156
3726
  const { project_id, job_id } = GetPipelineJobOutputSchema.parse(request.params.arguments);
3157
3727
  const jobDetails = await getPipelineJob(project_id, job_id);
@@ -3358,6 +3928,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3358
3928
  content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
3359
3929
  };
3360
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
+ }
3361
3945
  default:
3362
3946
  throw new Error(`Unknown tool: ${request.params.name}`);
3363
3947
  }
@@ -3390,7 +3974,7 @@ function determineTransportMode() {
3390
3974
  if (STREAMABLE_HTTP) {
3391
3975
  return TransportMode.STREAMABLE_HTTP;
3392
3976
  }
3393
- // Check for SSE support (medium priority)
3977
+ // Check for SSE support (medium priority)
3394
3978
  if (SSE) {
3395
3979
  return TransportMode.SSE;
3396
3980
  }
@@ -3432,7 +4016,7 @@ async function startSSEServer() {
3432
4016
  res.status(200).json({
3433
4017
  status: "healthy",
3434
4018
  version: SERVER_VERSION,
3435
- transport: TransportMode.SSE
4019
+ transport: TransportMode.SSE,
3436
4020
  });
3437
4021
  });
3438
4022
  app.listen(Number(PORT), HOST, () => {
@@ -3451,8 +4035,8 @@ async function startStreamableHTTPServer() {
3451
4035
  // Configure Express middleware
3452
4036
  app.use(express.json());
3453
4037
  // Streamable HTTP endpoint - handles both session creation and message handling
3454
- app.post('/mcp', async (req, res) => {
3455
- const sessionId = req.headers['mcp-session-id'];
4038
+ app.post("/mcp", async (req, res) => {
4039
+ const sessionId = req.headers["mcp-session-id"];
3456
4040
  try {
3457
4041
  let transport;
3458
4042
  if (sessionId && streamableTransports[sessionId]) {
@@ -3467,7 +4051,7 @@ async function startStreamableHTTPServer() {
3467
4051
  onsessioninitialized: (newSessionId) => {
3468
4052
  streamableTransports[newSessionId] = transport;
3469
4053
  logger.warn(`Streamable HTTP session initialized: ${newSessionId}`);
3470
- }
4054
+ },
3471
4055
  });
3472
4056
  // Set up cleanup handler when transport closes
3473
4057
  transport.onclose = () => {
@@ -3483,10 +4067,10 @@ async function startStreamableHTTPServer() {
3483
4067
  }
3484
4068
  }
3485
4069
  catch (error) {
3486
- logger.error('Streamable HTTP error:', error);
4070
+ logger.error("Streamable HTTP error:", error);
3487
4071
  res.status(500).json({
3488
- error: 'Internal server error',
3489
- message: error instanceof Error ? error.message : 'Unknown error'
4072
+ error: "Internal server error",
4073
+ message: error instanceof Error ? error.message : "Unknown error",
3490
4074
  });
3491
4075
  }
3492
4076
  });
@@ -3496,7 +4080,7 @@ async function startStreamableHTTPServer() {
3496
4080
  status: "healthy",
3497
4081
  version: SERVER_VERSION,
3498
4082
  transport: TransportMode.STREAMABLE_HTTP,
3499
- activeSessions: Object.keys(streamableTransports).length
4083
+ activeSessions: Object.keys(streamableTransports).length,
3500
4084
  });
3501
4085
  });
3502
4086
  // Start server
@@ -3510,18 +4094,18 @@ async function startStreamableHTTPServer() {
3510
4094
  * Handle transport-specific initialization logic
3511
4095
  */
3512
4096
  async function initializeServerByTransportMode(mode) {
3513
- logger.info('Initializing server with transport mode:', mode);
4097
+ logger.info("Initializing server with transport mode:", mode);
3514
4098
  switch (mode) {
3515
4099
  case TransportMode.STDIO:
3516
- logger.warn('Starting GitLab MCP Server with stdio transport');
4100
+ logger.warn("Starting GitLab MCP Server with stdio transport");
3517
4101
  await startStdioServer();
3518
4102
  break;
3519
4103
  case TransportMode.SSE:
3520
- logger.warn('Starting GitLab MCP Server with SSE transport');
4104
+ logger.warn("Starting GitLab MCP Server with SSE transport");
3521
4105
  await startSSEServer();
3522
4106
  break;
3523
4107
  case TransportMode.STREAMABLE_HTTP:
3524
- logger.warn('Starting GitLab MCP Server with Streamable HTTP transport');
4108
+ logger.warn("Starting GitLab MCP Server with Streamable HTTP transport");
3525
4109
  await startStreamableHTTPServer();
3526
4110
  break;
3527
4111
  default:
@@ -3544,6 +4128,7 @@ async function runServer() {
3544
4128
  process.exit(1);
3545
4129
  }
3546
4130
  }
4131
+ // 下記の2行を追記
3547
4132
  runServer().catch(error => {
3548
4133
  logger.error("Fatal error in main():", error);
3549
4134
  process.exit(1);