@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/README.md +81 -67
- package/build/customSchemas.js +11 -4
- package/build/index.js +623 -38
- package/build/schemas.js +533 -106
- package/build/src/argon2wrapper.js +68 -0
- package/build/src/authentication.js +78 -0
- package/build/src/authhelpers.js +44 -0
- package/build/src/config.js +99 -0
- package/build/src/customSchemas.js +21 -0
- package/build/src/gitlabhandler.js +1649 -0
- package/build/src/gitlabsession.js +103 -0
- package/build/src/logger.js +11 -0
- package/build/src/mcpserver.js +1331 -0
- package/build/src/oauth.js +389 -0
- package/build/src/schemas.js +1678 -0
- package/package.json +3 -2
- package/build/utils.js +0 -9
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
|
|
7
|
+
import express from "express";
|
|
8
8
|
import fetchCookie from "fetch-cookie";
|
|
9
|
-
import
|
|
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 {
|
|
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,
|
|
26
|
+
GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
27
27
|
// Discussion Schemas
|
|
28
28
|
GitLabDiscussionNoteSchema, // Added
|
|
29
|
-
GitLabDiscussionSchema,
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
34
|
+
import { pino } from "pino";
|
|
34
35
|
const logger = pino({
|
|
35
|
-
level: process.env.LOG_LEVEL ||
|
|
36
|
+
level: process.env.LOG_LEVEL || "info",
|
|
36
37
|
transport: {
|
|
37
|
-
target:
|
|
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 ||
|
|
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 ===
|
|
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:
|
|
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(
|
|
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(
|
|
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, {
|
|
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(
|
|
3455
|
-
const sessionId = req.headers[
|
|
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(
|
|
4070
|
+
logger.error("Streamable HTTP error:", error);
|
|
3487
4071
|
res.status(500).json({
|
|
3488
|
-
error:
|
|
3489
|
-
message: error instanceof Error ? error.message :
|
|
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(
|
|
4097
|
+
logger.info("Initializing server with transport mode:", mode);
|
|
3514
4098
|
switch (mode) {
|
|
3515
4099
|
case TransportMode.STDIO:
|
|
3516
|
-
logger.warn(
|
|
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(
|
|
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(
|
|
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);
|