@zereight/mcp-gitlab 1.0.75 → 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 +13 -4
- package/build/index.js +673 -38
- package/build/schemas.js +553 -100
- 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",
|
|
@@ -558,6 +614,21 @@ const allTools = [
|
|
|
558
614
|
description: "Get changes/diffs of a specific commit",
|
|
559
615
|
inputSchema: zodToJsonSchema(GetCommitDiffSchema),
|
|
560
616
|
},
|
|
617
|
+
{
|
|
618
|
+
name: "list_group_iterations",
|
|
619
|
+
description: "List group iterations with filtering options",
|
|
620
|
+
inputSchema: zodToJsonSchema(ListGroupIterationsSchema),
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
name: "upload_markdown",
|
|
624
|
+
description: "Upload a file to a GitLab project for use in markdown content",
|
|
625
|
+
inputSchema: zodToJsonSchema(MarkdownUploadSchema),
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: "download_attachment",
|
|
629
|
+
description: "Download an uploaded file from a GitLab project by secret and filename",
|
|
630
|
+
inputSchema: zodToJsonSchema(DownloadAttachmentSchema),
|
|
631
|
+
},
|
|
561
632
|
];
|
|
562
633
|
// Define which tools are read-only
|
|
563
634
|
const readOnlyTools = [
|
|
@@ -568,6 +639,7 @@ const readOnlyTools = [
|
|
|
568
639
|
"get_branch_diffs",
|
|
569
640
|
"mr_discussions",
|
|
570
641
|
"list_issues",
|
|
642
|
+
"my_issues",
|
|
571
643
|
"list_merge_requests",
|
|
572
644
|
"get_issue",
|
|
573
645
|
"list_issue_links",
|
|
@@ -577,12 +649,14 @@ const readOnlyTools = [
|
|
|
577
649
|
"get_namespace",
|
|
578
650
|
"verify_namespace",
|
|
579
651
|
"get_project",
|
|
652
|
+
"list_projects",
|
|
653
|
+
"list_project_members",
|
|
580
654
|
"get_pipeline",
|
|
581
655
|
"list_pipelines",
|
|
582
656
|
"list_pipeline_jobs",
|
|
657
|
+
"list_pipeline_trigger_jobs",
|
|
583
658
|
"get_pipeline_job",
|
|
584
659
|
"get_pipeline_job_output",
|
|
585
|
-
"list_projects",
|
|
586
660
|
"list_labels",
|
|
587
661
|
"get_label",
|
|
588
662
|
"list_group_projects",
|
|
@@ -598,6 +672,9 @@ const readOnlyTools = [
|
|
|
598
672
|
"list_commits",
|
|
599
673
|
"get_commit",
|
|
600
674
|
"get_commit_diff",
|
|
675
|
+
"list_group_iterations",
|
|
676
|
+
"get_group_iteration",
|
|
677
|
+
"download_attachment",
|
|
601
678
|
];
|
|
602
679
|
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
|
603
680
|
const wikiToolNames = [
|
|
@@ -625,6 +702,7 @@ const pipelineToolNames = [
|
|
|
625
702
|
"list_pipelines",
|
|
626
703
|
"get_pipeline",
|
|
627
704
|
"list_pipeline_jobs",
|
|
705
|
+
"list_pipeline_trigger_jobs",
|
|
628
706
|
"get_pipeline_job",
|
|
629
707
|
"get_pipeline_job_output",
|
|
630
708
|
"create_pipeline",
|
|
@@ -653,6 +731,7 @@ function normalizeGitLabApiUrl(url) {
|
|
|
653
731
|
// Use the normalizeGitLabApiUrl function to handle various URL formats
|
|
654
732
|
const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || "");
|
|
655
733
|
const GITLAB_PROJECT_ID = process.env.GITLAB_PROJECT_ID;
|
|
734
|
+
const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split(',').map(id => id.trim()).filter(Boolean) || [];
|
|
656
735
|
if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
|
|
657
736
|
logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
|
|
658
737
|
process.exit(1);
|
|
@@ -682,8 +761,24 @@ async function handleGitLabError(response) {
|
|
|
682
761
|
/**
|
|
683
762
|
* @param {string} projectId - The project ID parameter passed to the function
|
|
684
763
|
* @returns {string} The project ID to use for the API call
|
|
764
|
+
* @throws {Error} If GITLAB_ALLOWED_PROJECT_IDS is set and the requested project is not in the whitelist
|
|
685
765
|
*/
|
|
686
766
|
function getEffectiveProjectId(projectId) {
|
|
767
|
+
if (GITLAB_ALLOWED_PROJECT_IDS.length > 0) {
|
|
768
|
+
// If there's only one allowed project, use it as default
|
|
769
|
+
if (GITLAB_ALLOWED_PROJECT_IDS.length === 1 && !projectId) {
|
|
770
|
+
return GITLAB_ALLOWED_PROJECT_IDS[0];
|
|
771
|
+
}
|
|
772
|
+
// If a project ID is provided, check if it's in the whitelist
|
|
773
|
+
if (projectId && !GITLAB_ALLOWED_PROJECT_IDS.includes(projectId)) {
|
|
774
|
+
throw new Error(`Access denied: Project ${projectId} is not in the allowed project list: ${GITLAB_ALLOWED_PROJECT_IDS.join(', ')}`);
|
|
775
|
+
}
|
|
776
|
+
// If no project ID provided but we have multiple allowed projects, require an explicit choice
|
|
777
|
+
if (!projectId && GITLAB_ALLOWED_PROJECT_IDS.length > 1) {
|
|
778
|
+
throw new Error(`Multiple projects allowed (${GITLAB_ALLOWED_PROJECT_IDS.join(', ')}). Please specify a project ID.`);
|
|
779
|
+
}
|
|
780
|
+
return projectId || GITLAB_ALLOWED_PROJECT_IDS[0];
|
|
781
|
+
}
|
|
687
782
|
return GITLAB_PROJECT_ID || projectId;
|
|
688
783
|
}
|
|
689
784
|
/**
|
|
@@ -1616,6 +1711,26 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
|
|
|
1616
1711
|
await handleGitLabError(response);
|
|
1617
1712
|
return GitLabMergeRequestSchema.parse(await response.json());
|
|
1618
1713
|
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Merge a merge request
|
|
1716
|
+
* マージリクエストをマージする
|
|
1717
|
+
*
|
|
1718
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1719
|
+
* @param {number} mergeRequestIid - The internal ID of the merge request
|
|
1720
|
+
* @param {Object} options - Options for merging the merge request
|
|
1721
|
+
* @returns {Promise<GitLabMergeRequest>} The merged merge request
|
|
1722
|
+
*/
|
|
1723
|
+
async function mergeMergeRequest(projectId, options, mergeRequestIid) {
|
|
1724
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1725
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/merge`);
|
|
1726
|
+
const response = await fetch(url.toString(), {
|
|
1727
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1728
|
+
method: "PUT",
|
|
1729
|
+
body: JSON.stringify(options),
|
|
1730
|
+
});
|
|
1731
|
+
await handleGitLabError(response);
|
|
1732
|
+
return GitLabMergeRequestSchema.parse(await response.json());
|
|
1733
|
+
}
|
|
1619
1734
|
/**
|
|
1620
1735
|
* Create a new note (comment) on an issue or merge request
|
|
1621
1736
|
* 📦 새로운 함수: createNote - 이슈 또는 병합 요청에 노트(댓글)를 추가하는 함수
|
|
@@ -1644,6 +1759,208 @@ noteableIid, body) {
|
|
|
1644
1759
|
}
|
|
1645
1760
|
return await response.json();
|
|
1646
1761
|
}
|
|
1762
|
+
/**
|
|
1763
|
+
* List draft notes for a merge request
|
|
1764
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1765
|
+
* @param {number|string} mergeRequestIid - The internal ID of the merge request
|
|
1766
|
+
* @returns {Promise<GitLabDraftNote[]>} Array of draft notes
|
|
1767
|
+
*/
|
|
1768
|
+
async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
|
|
1769
|
+
const response = await fetch(`/projects/${encodeURIComponent(project_id)}/merge_requests/${merge_request_iid}/draft_notes/${draft_note_id}`);
|
|
1770
|
+
if (!response.ok) {
|
|
1771
|
+
const errorText = await response.text();
|
|
1772
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
1773
|
+
}
|
|
1774
|
+
const data = await response.json();
|
|
1775
|
+
return GitLabDraftNoteSchema.parse(data);
|
|
1776
|
+
}
|
|
1777
|
+
async function listDraftNotes(projectId, mergeRequestIid) {
|
|
1778
|
+
projectId = decodeURIComponent(projectId);
|
|
1779
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
|
|
1780
|
+
const response = await fetch(url.toString(), {
|
|
1781
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1782
|
+
method: "GET",
|
|
1783
|
+
});
|
|
1784
|
+
if (!response.ok) {
|
|
1785
|
+
const errorText = await response.text();
|
|
1786
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
1787
|
+
}
|
|
1788
|
+
const data = await response.json();
|
|
1789
|
+
return z.array(GitLabDraftNoteSchema).parse(data);
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Create a draft note for a merge request
|
|
1793
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1794
|
+
* @param {number|string} mergeRequestIid - The internal ID of the merge request
|
|
1795
|
+
* @param {string} body - The content of the draft note
|
|
1796
|
+
* @param {MergeRequestThreadPosition} [position] - Position information for diff notes
|
|
1797
|
+
* @param {boolean} [resolveDiscussion] - Whether to resolve the discussion when publishing
|
|
1798
|
+
* @returns {Promise<GitLabDraftNote>} The created draft note
|
|
1799
|
+
*/
|
|
1800
|
+
async function createDraftNote(projectId, mergeRequestIid, body, position, resolveDiscussion) {
|
|
1801
|
+
projectId = decodeURIComponent(projectId);
|
|
1802
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
|
|
1803
|
+
const requestBody = { note: body };
|
|
1804
|
+
if (position) {
|
|
1805
|
+
requestBody.position = position;
|
|
1806
|
+
}
|
|
1807
|
+
if (resolveDiscussion !== undefined) {
|
|
1808
|
+
requestBody.resolve_discussion = resolveDiscussion;
|
|
1809
|
+
}
|
|
1810
|
+
const response = await fetch(url.toString(), {
|
|
1811
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1812
|
+
method: "POST",
|
|
1813
|
+
body: JSON.stringify(requestBody),
|
|
1814
|
+
});
|
|
1815
|
+
if (!response.ok) {
|
|
1816
|
+
const errorText = await response.text();
|
|
1817
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
1818
|
+
}
|
|
1819
|
+
const data = await response.json();
|
|
1820
|
+
return GitLabDraftNoteSchema.parse(data);
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Update an existing draft note
|
|
1824
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1825
|
+
* @param {number|string} mergeRequestIid - The internal ID of the merge request
|
|
1826
|
+
* @param {number|string} draftNoteId - The ID of the draft note
|
|
1827
|
+
* @param {string} [body] - The updated content of the draft note
|
|
1828
|
+
* @param {MergeRequestThreadPosition} [position] - Updated position information
|
|
1829
|
+
* @param {boolean} [resolveDiscussion] - Whether to resolve the discussion when publishing
|
|
1830
|
+
* @returns {Promise<GitLabDraftNote>} The updated draft note
|
|
1831
|
+
*/
|
|
1832
|
+
async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, position, resolveDiscussion) {
|
|
1833
|
+
projectId = decodeURIComponent(projectId);
|
|
1834
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
|
|
1835
|
+
const requestBody = {};
|
|
1836
|
+
if (body !== undefined) {
|
|
1837
|
+
requestBody.note = body;
|
|
1838
|
+
}
|
|
1839
|
+
if (position) {
|
|
1840
|
+
requestBody.position = position;
|
|
1841
|
+
}
|
|
1842
|
+
if (resolveDiscussion !== undefined) {
|
|
1843
|
+
requestBody.resolve_discussion = resolveDiscussion;
|
|
1844
|
+
}
|
|
1845
|
+
const response = await fetch(url.toString(), {
|
|
1846
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1847
|
+
method: "PUT",
|
|
1848
|
+
body: JSON.stringify(requestBody),
|
|
1849
|
+
});
|
|
1850
|
+
if (!response.ok) {
|
|
1851
|
+
const errorText = await response.text();
|
|
1852
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
1853
|
+
}
|
|
1854
|
+
const data = await response.json();
|
|
1855
|
+
return GitLabDraftNoteSchema.parse(data);
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Delete a draft note
|
|
1859
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1860
|
+
* @param {number|string} mergeRequestIid - The internal ID of the merge request
|
|
1861
|
+
* @param {number|string} draftNoteId - The ID of the draft note
|
|
1862
|
+
* @returns {Promise<void>}
|
|
1863
|
+
*/
|
|
1864
|
+
async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
1865
|
+
projectId = decodeURIComponent(projectId);
|
|
1866
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
|
|
1867
|
+
const response = await fetch(url.toString(), {
|
|
1868
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1869
|
+
method: "DELETE",
|
|
1870
|
+
});
|
|
1871
|
+
if (!response.ok) {
|
|
1872
|
+
const errorText = await response.text();
|
|
1873
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Publish a single draft note
|
|
1878
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1879
|
+
* @param {number|string} mergeRequestIid - The internal ID of the merge request
|
|
1880
|
+
* @param {number|string} draftNoteId - The ID of the draft note
|
|
1881
|
+
* @returns {Promise<GitLabDiscussionNote>} The published note
|
|
1882
|
+
*/
|
|
1883
|
+
async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
1884
|
+
projectId = decodeURIComponent(projectId);
|
|
1885
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}/publish`);
|
|
1886
|
+
const response = await fetch(url.toString(), {
|
|
1887
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1888
|
+
method: "PUT",
|
|
1889
|
+
});
|
|
1890
|
+
if (!response.ok) {
|
|
1891
|
+
const errorText = await response.text();
|
|
1892
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
1893
|
+
}
|
|
1894
|
+
// Handle empty response (204 No Content) or successful response
|
|
1895
|
+
const responseText = await response.text();
|
|
1896
|
+
if (!responseText || responseText.trim() === '') {
|
|
1897
|
+
// Return a success indicator for empty responses
|
|
1898
|
+
return {
|
|
1899
|
+
id: draftNoteId.toString(),
|
|
1900
|
+
body: "Draft note published successfully",
|
|
1901
|
+
author: { id: "unknown", username: "unknown" },
|
|
1902
|
+
created_at: new Date().toISOString(),
|
|
1903
|
+
updated_at: new Date().toISOString(),
|
|
1904
|
+
system: false,
|
|
1905
|
+
noteable_id: mergeRequestIid.toString(),
|
|
1906
|
+
noteable_type: "MergeRequest"
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
try {
|
|
1910
|
+
const data = JSON.parse(responseText);
|
|
1911
|
+
return GitLabDiscussionNoteSchema.parse(data);
|
|
1912
|
+
}
|
|
1913
|
+
catch (parseError) {
|
|
1914
|
+
// If JSON parsing fails but the operation was successful (2xx status),
|
|
1915
|
+
// return a success indicator
|
|
1916
|
+
console.warn(`JSON parse error for successful publish operation: ${parseError}`);
|
|
1917
|
+
return {
|
|
1918
|
+
id: draftNoteId.toString(),
|
|
1919
|
+
body: "Draft note published successfully (response parse error)",
|
|
1920
|
+
author: { id: "unknown", username: "unknown" },
|
|
1921
|
+
created_at: new Date().toISOString(),
|
|
1922
|
+
updated_at: new Date().toISOString(),
|
|
1923
|
+
system: false,
|
|
1924
|
+
noteable_id: mergeRequestIid.toString(),
|
|
1925
|
+
noteable_type: "MergeRequest"
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Publish all draft notes for a merge request
|
|
1931
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1932
|
+
* @param {number|string} mergeRequestIid - The internal ID of the merge request
|
|
1933
|
+
* @returns {Promise<GitLabDiscussionNote[]>} Array of published notes
|
|
1934
|
+
*/
|
|
1935
|
+
async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
|
|
1936
|
+
projectId = decodeURIComponent(projectId);
|
|
1937
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/bulk_publish`);
|
|
1938
|
+
const response = await fetch(url.toString(), {
|
|
1939
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1940
|
+
method: "POST", // Changed from PUT to POST
|
|
1941
|
+
body: JSON.stringify({}), // Send empty body for POST request
|
|
1942
|
+
});
|
|
1943
|
+
if (!response.ok) {
|
|
1944
|
+
const errorText = await response.text();
|
|
1945
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
1946
|
+
}
|
|
1947
|
+
// Handle empty response (204 No Content) or successful response
|
|
1948
|
+
const responseText = await response.text();
|
|
1949
|
+
if (!responseText || responseText.trim() === '') {
|
|
1950
|
+
// Return empty array for successful bulk publish with no content
|
|
1951
|
+
return [];
|
|
1952
|
+
}
|
|
1953
|
+
try {
|
|
1954
|
+
const data = JSON.parse(responseText);
|
|
1955
|
+
return z.array(GitLabDiscussionNoteSchema).parse(data);
|
|
1956
|
+
}
|
|
1957
|
+
catch (parseError) {
|
|
1958
|
+
// If JSON parsing fails but the operation was successful (2xx status),
|
|
1959
|
+
// return empty array indicating successful bulk publish
|
|
1960
|
+
console.warn(`JSON parse error for successful bulk publish operation: ${parseError}`);
|
|
1961
|
+
return [];
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1647
1964
|
/**
|
|
1648
1965
|
* Create a new thread on a merge request
|
|
1649
1966
|
* 📦 새로운 함수: createMergeRequestThread - 병합 요청에 새로운 스레드(토론)를 생성하는 함수
|
|
@@ -2123,6 +2440,38 @@ async function listPipelineJobs(projectId, pipelineId, options = {}) {
|
|
|
2123
2440
|
const data = await response.json();
|
|
2124
2441
|
return z.array(GitLabPipelineJobSchema).parse(data);
|
|
2125
2442
|
}
|
|
2443
|
+
/**
|
|
2444
|
+
* List all trigger jobs (bridges) in a specific pipeline
|
|
2445
|
+
*
|
|
2446
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
2447
|
+
* @param {number} pipelineId - The ID of the pipeline
|
|
2448
|
+
* @param {Object} options - Options for filtering trigger jobs
|
|
2449
|
+
* @returns {Promise<GitLabPipelineTriggerJob[]>} List of pipeline trigger jobs
|
|
2450
|
+
*/
|
|
2451
|
+
async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
|
|
2452
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
2453
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/bridges`);
|
|
2454
|
+
// Add all query parameters
|
|
2455
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
2456
|
+
if (value !== undefined) {
|
|
2457
|
+
if (typeof value === "boolean") {
|
|
2458
|
+
url.searchParams.append(key, value ? "true" : "false");
|
|
2459
|
+
}
|
|
2460
|
+
else {
|
|
2461
|
+
url.searchParams.append(key, value.toString());
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
});
|
|
2465
|
+
const response = await fetch(url.toString(), {
|
|
2466
|
+
...DEFAULT_FETCH_CONFIG,
|
|
2467
|
+
});
|
|
2468
|
+
if (response.status === 404) {
|
|
2469
|
+
throw new Error(`Pipeline not found`);
|
|
2470
|
+
}
|
|
2471
|
+
await handleGitLabError(response);
|
|
2472
|
+
const data = await response.json();
|
|
2473
|
+
return z.array(GitLabPipelineTriggerJobSchema).parse(data);
|
|
2474
|
+
}
|
|
2126
2475
|
async function getPipelineJob(projectId, jobId) {
|
|
2127
2476
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
2128
2477
|
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}`);
|
|
@@ -2162,14 +2511,14 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
|
|
|
2162
2511
|
const fullTrace = await response.text();
|
|
2163
2512
|
// Apply client-side pagination to limit context window usage
|
|
2164
2513
|
if (limit !== undefined || offset !== undefined) {
|
|
2165
|
-
const lines = fullTrace.split(
|
|
2514
|
+
const lines = fullTrace.split("\n");
|
|
2166
2515
|
const startOffset = offset || 0;
|
|
2167
2516
|
const maxLines = limit || 1000;
|
|
2168
2517
|
// Return lines from the end, skipping offset lines and limiting to maxLines
|
|
2169
2518
|
const startIndex = Math.max(0, lines.length - startOffset - maxLines);
|
|
2170
2519
|
const endIndex = lines.length - startOffset;
|
|
2171
2520
|
const selectedLines = lines.slice(startIndex, endIndex);
|
|
2172
|
-
const result = selectedLines.join(
|
|
2521
|
+
const result = selectedLines.join("\n");
|
|
2173
2522
|
// Add metadata about truncation
|
|
2174
2523
|
if (startIndex > 0 || endIndex < lines.length) {
|
|
2175
2524
|
const totalLines = lines.length;
|
|
@@ -2580,6 +2929,175 @@ async function getCommitDiff(projectId, sha) {
|
|
|
2580
2929
|
const data = await response.json();
|
|
2581
2930
|
return z.array(GitLabDiffSchema).parse(data);
|
|
2582
2931
|
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Get the current authenticated user
|
|
2934
|
+
* 현재 인증된 사용자 가져오기
|
|
2935
|
+
*
|
|
2936
|
+
* @returns {Promise<GitLabUser>} The current user
|
|
2937
|
+
*/
|
|
2938
|
+
async function getCurrentUser() {
|
|
2939
|
+
const response = await fetch(`${GITLAB_API_URL}/user`, DEFAULT_FETCH_CONFIG);
|
|
2940
|
+
await handleGitLabError(response);
|
|
2941
|
+
const data = await response.json();
|
|
2942
|
+
return GitLabUserSchema.parse(data);
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* List issues assigned to the current authenticated user
|
|
2946
|
+
* 현재 인증된 사용자에게 할당된 이슈 목록 조회
|
|
2947
|
+
*
|
|
2948
|
+
* @param {MyIssuesOptions} options - Options for filtering issues
|
|
2949
|
+
* @returns {Promise<GitLabIssue[]>} List of issues assigned to the current user
|
|
2950
|
+
*/
|
|
2951
|
+
async function myIssues(options = {}) {
|
|
2952
|
+
// Get current user to find their username
|
|
2953
|
+
const currentUser = await getCurrentUser();
|
|
2954
|
+
// Use getEffectiveProjectId to handle project ID resolution
|
|
2955
|
+
const effectiveProjectId = getEffectiveProjectId(options.project_id || "");
|
|
2956
|
+
// Use listIssues with assignee_username filter
|
|
2957
|
+
let listIssuesOptions = {
|
|
2958
|
+
state: options.state || "opened", // Default to "opened" if not specified
|
|
2959
|
+
labels: options.labels,
|
|
2960
|
+
milestone: options.milestone,
|
|
2961
|
+
search: options.search,
|
|
2962
|
+
created_after: options.created_after,
|
|
2963
|
+
created_before: options.created_before,
|
|
2964
|
+
updated_after: options.updated_after,
|
|
2965
|
+
updated_before: options.updated_before,
|
|
2966
|
+
per_page: options.per_page,
|
|
2967
|
+
page: options.page,
|
|
2968
|
+
};
|
|
2969
|
+
if (currentUser.username) {
|
|
2970
|
+
listIssuesOptions.assignee_username = [currentUser.username];
|
|
2971
|
+
}
|
|
2972
|
+
else {
|
|
2973
|
+
listIssuesOptions.assignee_id = currentUser.id;
|
|
2974
|
+
}
|
|
2975
|
+
return listIssues(effectiveProjectId, listIssuesOptions);
|
|
2976
|
+
}
|
|
2977
|
+
/**
|
|
2978
|
+
* List members of a GitLab project
|
|
2979
|
+
* GitLab 프로젝트 멤버 목록 조회
|
|
2980
|
+
*
|
|
2981
|
+
* @param {string} projectId - Project ID or URL-encoded path
|
|
2982
|
+
* @param {Omit<ListProjectMembersOptions, "project_id">} options - Options for filtering members
|
|
2983
|
+
* @returns {Promise<GitLabProjectMember[]>} List of project members
|
|
2984
|
+
*/
|
|
2985
|
+
async function listProjectMembers(projectId, options = {}) {
|
|
2986
|
+
projectId = decodeURIComponent(projectId);
|
|
2987
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
2988
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/members`);
|
|
2989
|
+
// Add query parameters
|
|
2990
|
+
if (options.query)
|
|
2991
|
+
url.searchParams.append("query", options.query);
|
|
2992
|
+
if (options.user_ids) {
|
|
2993
|
+
options.user_ids.forEach(id => url.searchParams.append("user_ids[]", id.toString()));
|
|
2994
|
+
}
|
|
2995
|
+
if (options.skip_users) {
|
|
2996
|
+
options.skip_users.forEach(id => url.searchParams.append("skip_users[]", id.toString()));
|
|
2997
|
+
}
|
|
2998
|
+
if (options.per_page)
|
|
2999
|
+
url.searchParams.append("per_page", options.per_page.toString());
|
|
3000
|
+
if (options.page)
|
|
3001
|
+
url.searchParams.append("page", options.page.toString());
|
|
3002
|
+
const response = await fetch(url.toString(), DEFAULT_FETCH_CONFIG);
|
|
3003
|
+
await handleGitLabError(response);
|
|
3004
|
+
const data = await response.json();
|
|
3005
|
+
return z.array(GitLabProjectMemberSchema).parse(data);
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* list group iterations
|
|
3009
|
+
*
|
|
3010
|
+
* @param {string} groupId
|
|
3011
|
+
* @param {Omit<ListGroupIterationsOptions, "group_id">} options
|
|
3012
|
+
* @returns {Promise<GetIt[]>}
|
|
3013
|
+
*/
|
|
3014
|
+
async function listGroupIterations(groupId, options = {}) {
|
|
3015
|
+
groupId = decodeURIComponent(groupId);
|
|
3016
|
+
const url = new URL(`${GITLAB_API_URL}/groups/${encodeURIComponent(groupId)}/iterations`);
|
|
3017
|
+
// クエリパラメータの追加
|
|
3018
|
+
if (options.state)
|
|
3019
|
+
url.searchParams.append("state", options.state);
|
|
3020
|
+
if (options.search)
|
|
3021
|
+
url.searchParams.append("search", options.search);
|
|
3022
|
+
if (options.in)
|
|
3023
|
+
url.searchParams.append("in", options.in.join(","));
|
|
3024
|
+
if (options.include_ancestors !== undefined)
|
|
3025
|
+
url.searchParams.append("include_ancestors", options.include_ancestors.toString());
|
|
3026
|
+
if (options.include_descendants !== undefined)
|
|
3027
|
+
url.searchParams.append("include_descendants", options.include_descendants.toString());
|
|
3028
|
+
if (options.updated_before)
|
|
3029
|
+
url.searchParams.append("updated_before", options.updated_before);
|
|
3030
|
+
if (options.updated_after)
|
|
3031
|
+
url.searchParams.append("updated_after", options.updated_after);
|
|
3032
|
+
if (options.page)
|
|
3033
|
+
url.searchParams.append("page", options.page.toString());
|
|
3034
|
+
if (options.per_page)
|
|
3035
|
+
url.searchParams.append("per_page", options.per_page.toString());
|
|
3036
|
+
const response = await fetch(url.toString(), DEFAULT_FETCH_CONFIG);
|
|
3037
|
+
if (!response.ok) {
|
|
3038
|
+
await handleGitLabError(response);
|
|
3039
|
+
}
|
|
3040
|
+
const data = await response.json();
|
|
3041
|
+
return z.array(GroupIteration).parse(data);
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Upload a file to a GitLab project for use in markdown content
|
|
3045
|
+
*
|
|
3046
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3047
|
+
* @param {string} filePath - Path to the local file to upload
|
|
3048
|
+
* @returns {Promise<GitLabMarkdownUpload>} The upload response
|
|
3049
|
+
*/
|
|
3050
|
+
async function markdownUpload(projectId, filePath) {
|
|
3051
|
+
projectId = decodeURIComponent(projectId);
|
|
3052
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
3053
|
+
// Check if file exists
|
|
3054
|
+
if (!fs.existsSync(filePath)) {
|
|
3055
|
+
throw new Error(`File not found: ${filePath}`);
|
|
3056
|
+
}
|
|
3057
|
+
// Read the file
|
|
3058
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
3059
|
+
const fileName = path.basename(filePath);
|
|
3060
|
+
// Create form data
|
|
3061
|
+
const FormData = (await import("form-data")).default;
|
|
3062
|
+
const form = new FormData();
|
|
3063
|
+
form.append("file", fileBuffer, {
|
|
3064
|
+
filename: fileName,
|
|
3065
|
+
contentType: "application/octet-stream",
|
|
3066
|
+
});
|
|
3067
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/uploads`);
|
|
3068
|
+
const response = await fetch(url.toString(), {
|
|
3069
|
+
method: "POST",
|
|
3070
|
+
headers: {
|
|
3071
|
+
...DEFAULT_HEADERS,
|
|
3072
|
+
// Remove Content-Type header to let form-data set it with boundary
|
|
3073
|
+
"Content-Type": undefined,
|
|
3074
|
+
},
|
|
3075
|
+
body: form,
|
|
3076
|
+
});
|
|
3077
|
+
if (!response.ok) {
|
|
3078
|
+
await handleGitLabError(response);
|
|
3079
|
+
}
|
|
3080
|
+
const data = await response.json();
|
|
3081
|
+
return GitLabMarkdownUploadSchema.parse(data);
|
|
3082
|
+
}
|
|
3083
|
+
async function downloadAttachment(projectId, secret, filename, localPath) {
|
|
3084
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
3085
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${secret}/${filename}`);
|
|
3086
|
+
const response = await fetch(url.toString(), {
|
|
3087
|
+
method: "GET",
|
|
3088
|
+
headers: DEFAULT_HEADERS,
|
|
3089
|
+
});
|
|
3090
|
+
if (!response.ok) {
|
|
3091
|
+
await handleGitLabError(response);
|
|
3092
|
+
}
|
|
3093
|
+
// Get the file content as buffer
|
|
3094
|
+
const buffer = await response.arrayBuffer();
|
|
3095
|
+
// Determine the save path
|
|
3096
|
+
const savePath = localPath ? path.join(localPath, filename) : filename;
|
|
3097
|
+
// Write the file to disk
|
|
3098
|
+
fs.writeFileSync(savePath, Buffer.from(buffer));
|
|
3099
|
+
return savePath;
|
|
3100
|
+
}
|
|
2583
3101
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2584
3102
|
// Apply read-only filter first
|
|
2585
3103
|
const tools0 = GITLAB_READ_ONLY_MODE
|
|
@@ -2798,6 +3316,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2798
3316
|
content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
|
|
2799
3317
|
};
|
|
2800
3318
|
}
|
|
3319
|
+
case "merge_merge_request": {
|
|
3320
|
+
const args = MergeMergeRequestSchema.parse(request.params.arguments);
|
|
3321
|
+
const { project_id, merge_request_iid, ...options } = args;
|
|
3322
|
+
const mergeRequest = await mergeMergeRequest(project_id, options, merge_request_iid);
|
|
3323
|
+
return {
|
|
3324
|
+
content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
2801
3327
|
case "mr_discussions": {
|
|
2802
3328
|
const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments);
|
|
2803
3329
|
const { project_id, merge_request_iid, ...options } = args;
|
|
@@ -2877,6 +3403,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2877
3403
|
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
|
|
2878
3404
|
};
|
|
2879
3405
|
}
|
|
3406
|
+
case "list_project_members": {
|
|
3407
|
+
const args = ListProjectMembersSchema.parse(request.params.arguments);
|
|
3408
|
+
const { project_id, ...options } = args;
|
|
3409
|
+
const members = await listProjectMembers(project_id, options);
|
|
3410
|
+
return {
|
|
3411
|
+
content: [{ type: "text", text: JSON.stringify(members, null, 2) }],
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
2880
3414
|
case "get_users": {
|
|
2881
3415
|
const args = GetUsersSchema.parse(request.params.arguments);
|
|
2882
3416
|
const usersMap = await getUsers(args.usernames);
|
|
@@ -2892,6 +3426,62 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2892
3426
|
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
2893
3427
|
};
|
|
2894
3428
|
}
|
|
3429
|
+
case "get_draft_note": {
|
|
3430
|
+
const args = GetDraftNoteSchema.parse(request.params.arguments);
|
|
3431
|
+
const { project_id, merge_request_iid, draft_note_id } = args;
|
|
3432
|
+
const draftNote = await getDraftNote(project_id, merge_request_iid, draft_note_id);
|
|
3433
|
+
return {
|
|
3434
|
+
content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
case "list_draft_notes": {
|
|
3438
|
+
const args = ListDraftNotesSchema.parse(request.params.arguments);
|
|
3439
|
+
const { project_id, merge_request_iid } = args;
|
|
3440
|
+
const draftNotes = await listDraftNotes(project_id, merge_request_iid);
|
|
3441
|
+
return {
|
|
3442
|
+
content: [{ type: "text", text: JSON.stringify(draftNotes, null, 2) }],
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
case "create_draft_note": {
|
|
3446
|
+
const args = CreateDraftNoteSchema.parse(request.params.arguments);
|
|
3447
|
+
const { project_id, merge_request_iid, body, position, resolve_discussion } = args;
|
|
3448
|
+
const draftNote = await createDraftNote(project_id, merge_request_iid, body, position, resolve_discussion);
|
|
3449
|
+
return {
|
|
3450
|
+
content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
case "update_draft_note": {
|
|
3454
|
+
const args = UpdateDraftNoteSchema.parse(request.params.arguments);
|
|
3455
|
+
const { project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion } = args;
|
|
3456
|
+
const draftNote = await updateDraftNote(project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion);
|
|
3457
|
+
return {
|
|
3458
|
+
content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3461
|
+
case "delete_draft_note": {
|
|
3462
|
+
const args = DeleteDraftNoteSchema.parse(request.params.arguments);
|
|
3463
|
+
const { project_id, merge_request_iid, draft_note_id } = args;
|
|
3464
|
+
await deleteDraftNote(project_id, merge_request_iid, draft_note_id);
|
|
3465
|
+
return {
|
|
3466
|
+
content: [{ type: "text", text: "Draft note deleted successfully" }],
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
case "publish_draft_note": {
|
|
3470
|
+
const args = PublishDraftNoteSchema.parse(request.params.arguments);
|
|
3471
|
+
const { project_id, merge_request_iid, draft_note_id } = args;
|
|
3472
|
+
const publishedNote = await publishDraftNote(project_id, merge_request_iid, draft_note_id);
|
|
3473
|
+
return {
|
|
3474
|
+
content: [{ type: "text", text: JSON.stringify(publishedNote, null, 2) }],
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
case "bulk_publish_draft_notes": {
|
|
3478
|
+
const args = BulkPublishDraftNotesSchema.parse(request.params.arguments);
|
|
3479
|
+
const { project_id, merge_request_iid } = args;
|
|
3480
|
+
const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid);
|
|
3481
|
+
return {
|
|
3482
|
+
content: [{ type: "text", text: JSON.stringify(publishedNotes, null, 2) }],
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
2895
3485
|
case "create_merge_request_thread": {
|
|
2896
3486
|
const args = CreateMergeRequestThreadSchema.parse(request.params.arguments);
|
|
2897
3487
|
const { project_id, merge_request_iid, body, position, created_at } = args;
|
|
@@ -2908,6 +3498,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2908
3498
|
content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
|
|
2909
3499
|
};
|
|
2910
3500
|
}
|
|
3501
|
+
case "my_issues": {
|
|
3502
|
+
const args = MyIssuesSchema.parse(request.params.arguments);
|
|
3503
|
+
const issues = await myIssues(args);
|
|
3504
|
+
return {
|
|
3505
|
+
content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
2911
3508
|
case "get_issue": {
|
|
2912
3509
|
const args = GetIssueSchema.parse(request.params.arguments);
|
|
2913
3510
|
const issue = await getIssue(args.project_id, args.issue_iid);
|
|
@@ -3029,7 +3626,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3029
3626
|
}
|
|
3030
3627
|
case "list_wiki_pages": {
|
|
3031
3628
|
const { project_id, page, per_page, with_content } = ListWikiPagesSchema.parse(request.params.arguments);
|
|
3032
|
-
const wikiPages = await listWikiPages(project_id, {
|
|
3629
|
+
const wikiPages = await listWikiPages(project_id, {
|
|
3630
|
+
page,
|
|
3631
|
+
per_page,
|
|
3632
|
+
with_content,
|
|
3633
|
+
});
|
|
3033
3634
|
return {
|
|
3034
3635
|
content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
|
|
3035
3636
|
};
|
|
@@ -3109,6 +3710,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3109
3710
|
],
|
|
3110
3711
|
};
|
|
3111
3712
|
}
|
|
3713
|
+
case "list_pipeline_trigger_jobs": {
|
|
3714
|
+
const { project_id, pipeline_id, ...options } = ListPipelineTriggerJobsSchema.parse(request.params.arguments);
|
|
3715
|
+
const triggerJobs = await listPipelineTriggerJobs(project_id, pipeline_id, options);
|
|
3716
|
+
return {
|
|
3717
|
+
content: [
|
|
3718
|
+
{
|
|
3719
|
+
type: "text",
|
|
3720
|
+
text: JSON.stringify(triggerJobs, null, 2),
|
|
3721
|
+
},
|
|
3722
|
+
],
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
3112
3725
|
case "get_pipeline_job": {
|
|
3113
3726
|
const { project_id, job_id } = GetPipelineJobOutputSchema.parse(request.params.arguments);
|
|
3114
3727
|
const jobDetails = await getPipelineJob(project_id, job_id);
|
|
@@ -3308,6 +3921,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3308
3921
|
content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
|
|
3309
3922
|
};
|
|
3310
3923
|
}
|
|
3924
|
+
case "list_group_iterations": {
|
|
3925
|
+
const args = ListGroupIterationsSchema.parse(request.params.arguments);
|
|
3926
|
+
const iterations = await listGroupIterations(args.group_id, args);
|
|
3927
|
+
return {
|
|
3928
|
+
content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
case "upload_markdown": {
|
|
3932
|
+
const args = MarkdownUploadSchema.parse(request.params.arguments);
|
|
3933
|
+
const upload = await markdownUpload(args.project_id, args.file_path);
|
|
3934
|
+
return {
|
|
3935
|
+
content: [{ type: "text", text: JSON.stringify(upload, null, 2) }],
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
case "download_attachment": {
|
|
3939
|
+
const args = DownloadAttachmentSchema.parse(request.params.arguments);
|
|
3940
|
+
const filePath = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
|
|
3941
|
+
return {
|
|
3942
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, file_path: filePath }, null, 2) }],
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3311
3945
|
default:
|
|
3312
3946
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
3313
3947
|
}
|
|
@@ -3340,7 +3974,7 @@ function determineTransportMode() {
|
|
|
3340
3974
|
if (STREAMABLE_HTTP) {
|
|
3341
3975
|
return TransportMode.STREAMABLE_HTTP;
|
|
3342
3976
|
}
|
|
3343
|
-
// Check for SSE support (medium priority)
|
|
3977
|
+
// Check for SSE support (medium priority)
|
|
3344
3978
|
if (SSE) {
|
|
3345
3979
|
return TransportMode.SSE;
|
|
3346
3980
|
}
|
|
@@ -3382,7 +4016,7 @@ async function startSSEServer() {
|
|
|
3382
4016
|
res.status(200).json({
|
|
3383
4017
|
status: "healthy",
|
|
3384
4018
|
version: SERVER_VERSION,
|
|
3385
|
-
transport: TransportMode.SSE
|
|
4019
|
+
transport: TransportMode.SSE,
|
|
3386
4020
|
});
|
|
3387
4021
|
});
|
|
3388
4022
|
app.listen(Number(PORT), HOST, () => {
|
|
@@ -3401,8 +4035,8 @@ async function startStreamableHTTPServer() {
|
|
|
3401
4035
|
// Configure Express middleware
|
|
3402
4036
|
app.use(express.json());
|
|
3403
4037
|
// Streamable HTTP endpoint - handles both session creation and message handling
|
|
3404
|
-
app.post(
|
|
3405
|
-
const sessionId = req.headers[
|
|
4038
|
+
app.post("/mcp", async (req, res) => {
|
|
4039
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
3406
4040
|
try {
|
|
3407
4041
|
let transport;
|
|
3408
4042
|
if (sessionId && streamableTransports[sessionId]) {
|
|
@@ -3417,7 +4051,7 @@ async function startStreamableHTTPServer() {
|
|
|
3417
4051
|
onsessioninitialized: (newSessionId) => {
|
|
3418
4052
|
streamableTransports[newSessionId] = transport;
|
|
3419
4053
|
logger.warn(`Streamable HTTP session initialized: ${newSessionId}`);
|
|
3420
|
-
}
|
|
4054
|
+
},
|
|
3421
4055
|
});
|
|
3422
4056
|
// Set up cleanup handler when transport closes
|
|
3423
4057
|
transport.onclose = () => {
|
|
@@ -3433,10 +4067,10 @@ async function startStreamableHTTPServer() {
|
|
|
3433
4067
|
}
|
|
3434
4068
|
}
|
|
3435
4069
|
catch (error) {
|
|
3436
|
-
logger.error(
|
|
4070
|
+
logger.error("Streamable HTTP error:", error);
|
|
3437
4071
|
res.status(500).json({
|
|
3438
|
-
error:
|
|
3439
|
-
message: error instanceof Error ? error.message :
|
|
4072
|
+
error: "Internal server error",
|
|
4073
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
3440
4074
|
});
|
|
3441
4075
|
}
|
|
3442
4076
|
});
|
|
@@ -3446,7 +4080,7 @@ async function startStreamableHTTPServer() {
|
|
|
3446
4080
|
status: "healthy",
|
|
3447
4081
|
version: SERVER_VERSION,
|
|
3448
4082
|
transport: TransportMode.STREAMABLE_HTTP,
|
|
3449
|
-
activeSessions: Object.keys(streamableTransports).length
|
|
4083
|
+
activeSessions: Object.keys(streamableTransports).length,
|
|
3450
4084
|
});
|
|
3451
4085
|
});
|
|
3452
4086
|
// Start server
|
|
@@ -3460,18 +4094,18 @@ async function startStreamableHTTPServer() {
|
|
|
3460
4094
|
* Handle transport-specific initialization logic
|
|
3461
4095
|
*/
|
|
3462
4096
|
async function initializeServerByTransportMode(mode) {
|
|
3463
|
-
logger.info(
|
|
4097
|
+
logger.info("Initializing server with transport mode:", mode);
|
|
3464
4098
|
switch (mode) {
|
|
3465
4099
|
case TransportMode.STDIO:
|
|
3466
|
-
logger.warn(
|
|
4100
|
+
logger.warn("Starting GitLab MCP Server with stdio transport");
|
|
3467
4101
|
await startStdioServer();
|
|
3468
4102
|
break;
|
|
3469
4103
|
case TransportMode.SSE:
|
|
3470
|
-
logger.warn(
|
|
4104
|
+
logger.warn("Starting GitLab MCP Server with SSE transport");
|
|
3471
4105
|
await startSSEServer();
|
|
3472
4106
|
break;
|
|
3473
4107
|
case TransportMode.STREAMABLE_HTTP:
|
|
3474
|
-
logger.warn(
|
|
4108
|
+
logger.warn("Starting GitLab MCP Server with Streamable HTTP transport");
|
|
3475
4109
|
await startStreamableHTTPServer();
|
|
3476
4110
|
break;
|
|
3477
4111
|
default:
|
|
@@ -3494,6 +4128,7 @@ async function runServer() {
|
|
|
3494
4128
|
process.exit(1);
|
|
3495
4129
|
}
|
|
3496
4130
|
}
|
|
4131
|
+
// 下記の2行を追記
|
|
3497
4132
|
runServer().catch(error => {
|
|
3498
4133
|
logger.error("Fatal error in main():", error);
|
|
3499
4134
|
process.exit(1);
|