@zereight/mcp-gitlab 2.0.30 → 2.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -36,21 +36,21 @@ import { CookieJar, parse as parseCookie } from "tough-cookie";
36
36
  import { fileURLToPath, URL } from "node:url";
37
37
  import { z } from "zod";
38
38
  import { zodToJsonSchema } from "zod-to-json-schema";
39
- import { initializeOAuth } from "./oauth.js";
39
+ import { initializeOAuthClient } from "./oauth.js";
40
40
  import { GitLabClientPool } from "./gitlab-client-pool.js";
41
41
  // Add type imports for proxy agents
42
42
  import { Agent } from "node:http";
43
43
  import { Agent as HttpsAgent } from "node:https";
44
44
  import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateLabelSchema, // Added
45
- CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, DeleteDraftNoteSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema,
45
+ CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, DeleteDraftNoteSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema,
46
46
  // pipeline job schemas
47
47
  GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
48
48
  // Discussion Schemas
49
49
  GitLabDiscussionNoteSchema, // Added
50
50
  GitLabDiscussionSchema,
51
51
  // Draft Notes Schemas
52
- 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
53
- ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, } from "./schemas.js";
52
+ GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabTreeSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
53
+ ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, } from "./schemas.js";
54
54
  import { randomUUID } from "node:crypto";
55
55
  import { pino } from "pino";
56
56
  const logger = pino({
@@ -243,6 +243,28 @@ function validateConfiguration() {
243
243
  }
244
244
  const GITLAB_PERSONAL_ACCESS_TOKEN = getConfig("token", "GITLAB_PERSONAL_ACCESS_TOKEN");
245
245
  let OAUTH_ACCESS_TOKEN = null;
246
+ let oauthClient = null;
247
+ /**
248
+ * Ensure the OAuth token is valid before making an API call.
249
+ * Refreshes the token lazily (only when a tool is actually called).
250
+ * This avoids background timers that cause issues with multiple instances.
251
+ */
252
+ async function ensureValidOAuthToken() {
253
+ if (!oauthClient)
254
+ return;
255
+ if (oauthClient.hasValidToken())
256
+ return;
257
+ try {
258
+ logger.info("OAuth token expired or missing, refreshing...");
259
+ const freshToken = await oauthClient.getAccessToken();
260
+ OAUTH_ACCESS_TOKEN = freshToken;
261
+ logger.info("OAuth token refreshed successfully");
262
+ }
263
+ catch (error) {
264
+ logger.error("Failed to refresh OAuth token:", error);
265
+ throw error;
266
+ }
267
+ }
246
268
  const GITLAB_AUTH_COOKIE_PATH = getConfig("cookie-path", "GITLAB_AUTH_COOKIE_PATH");
247
269
  const USE_OAUTH = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
248
270
  const IS_OLD = getConfig("is-old", "GITLAB_IS_OLD") === "true";
@@ -457,7 +479,7 @@ const BASE_HEADERS = {
457
479
  /**
458
480
  * Build authentication headers dynamically based on context
459
481
  * In REMOTE_AUTHORIZATION mode, reads from AsyncLocalStorage session context
460
- * Otherwise, uses environment token
482
+ * Otherwise, uses environment token (OAuth token is refreshed lazily before each tool call)
461
483
  */
462
484
  function buildAuthHeaders() {
463
485
  if (REMOTE_AUTHORIZATION) {
@@ -572,7 +594,7 @@ const allTools = [
572
594
  },
573
595
  {
574
596
  name: "get_merge_request_approval_state",
575
- description: "Get the approval state of a merge request including approval rules and who has approved",
597
+ description: "Get merge request approval details including approvers (uses approval_state when available, falls back to approvals endpoint)",
576
598
  inputSchema: toJSONSchema(GetMergeRequestApprovalStateSchema),
577
599
  },
578
600
  {
@@ -627,7 +649,7 @@ const allTools = [
627
649
  },
628
650
  {
629
651
  name: "get_merge_request",
630
- description: "Get details of a merge request (Either mergeRequestIid or branchName must be provided)",
652
+ description: "Get details of a merge request with compact deployment, commit addition, and approval summaries (Either mergeRequestIid or branchName must be provided)",
631
653
  inputSchema: toJSONSchema(GetMergeRequestSchema),
632
654
  },
633
655
  {
@@ -915,6 +937,26 @@ const allTools = [
915
937
  description: "Get details of a specific pipeline in a GitLab project",
916
938
  inputSchema: toJSONSchema(GetPipelineSchema),
917
939
  },
940
+ {
941
+ name: "list_deployments",
942
+ description: "List deployments in a GitLab project with filtering options",
943
+ inputSchema: toJSONSchema(ListDeploymentsSchema),
944
+ },
945
+ {
946
+ name: "get_deployment",
947
+ description: "Get details of a specific deployment in a GitLab project",
948
+ inputSchema: toJSONSchema(GetDeploymentSchema),
949
+ },
950
+ {
951
+ name: "list_environments",
952
+ description: "List environments in a GitLab project",
953
+ inputSchema: toJSONSchema(ListEnvironmentsSchema),
954
+ },
955
+ {
956
+ name: "get_environment",
957
+ description: "Get details of a specific environment in a GitLab project",
958
+ inputSchema: toJSONSchema(GetEnvironmentSchema),
959
+ },
918
960
  {
919
961
  name: "list_pipeline_jobs",
920
962
  description: "List all jobs in a specific pipeline",
@@ -965,6 +1007,21 @@ const allTools = [
965
1007
  description: "Cancel a running pipeline job",
966
1008
  inputSchema: toJSONSchema(CancelPipelineJobSchema),
967
1009
  },
1010
+ {
1011
+ name: "list_job_artifacts",
1012
+ description: "List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes.",
1013
+ inputSchema: toJSONSchema(ListJobArtifactsSchema),
1014
+ },
1015
+ {
1016
+ name: "download_job_artifacts",
1017
+ description: "Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path.",
1018
+ inputSchema: toJSONSchema(DownloadJobArtifactsSchema),
1019
+ },
1020
+ {
1021
+ name: "get_job_artifact_file",
1022
+ description: "Get the content of a single file from a job's artifacts by its path within the archive",
1023
+ inputSchema: toJSONSchema(GetJobArtifactFileSchema),
1024
+ },
968
1025
  {
969
1026
  name: "list_merge_requests",
970
1027
  description: "List merge requests. Without project_id, lists MRs assigned to the authenticated user by default (use scope='all' for all accessible MRs). With project_id, lists MRs for that specific project.",
@@ -1122,10 +1179,17 @@ const readOnlyTools = new Set([
1122
1179
  "list_project_members",
1123
1180
  "get_pipeline",
1124
1181
  "list_pipelines",
1182
+ "list_deployments",
1183
+ "get_deployment",
1184
+ "list_environments",
1185
+ "get_environment",
1125
1186
  "list_pipeline_jobs",
1126
1187
  "list_pipeline_trigger_jobs",
1127
1188
  "get_pipeline_job",
1128
1189
  "get_pipeline_job_output",
1190
+ "list_job_artifacts",
1191
+ "download_job_artifacts",
1192
+ "get_job_artifact_file",
1129
1193
  "list_labels",
1130
1194
  "get_label",
1131
1195
  "list_group_projects",
@@ -1176,6 +1240,10 @@ const milestoneToolNames = new Set([
1176
1240
  const pipelineToolNames = new Set([
1177
1241
  "list_pipelines",
1178
1242
  "get_pipeline",
1243
+ "list_deployments",
1244
+ "get_deployment",
1245
+ "list_environments",
1246
+ "get_environment",
1179
1247
  "list_pipeline_jobs",
1180
1248
  "list_pipeline_trigger_jobs",
1181
1249
  "get_pipeline_job",
@@ -1186,6 +1254,9 @@ const pipelineToolNames = new Set([
1186
1254
  "play_pipeline_job",
1187
1255
  "retry_pipeline_job",
1188
1256
  "cancel_pipeline_job",
1257
+ "list_job_artifacts",
1258
+ "download_job_artifacts",
1259
+ "get_job_artifact_file",
1189
1260
  ]);
1190
1261
  const TOOLSET_DEFINITIONS = [
1191
1262
  {
@@ -1295,10 +1366,14 @@ const TOOLSET_DEFINITIONS = [
1295
1366
  },
1296
1367
  {
1297
1368
  id: "pipelines",
1298
- isDefault: false,
1369
+ isDefault: true,
1299
1370
  tools: new Set([
1300
1371
  "list_pipelines",
1301
1372
  "get_pipeline",
1373
+ "list_deployments",
1374
+ "get_deployment",
1375
+ "list_environments",
1376
+ "get_environment",
1302
1377
  "list_pipeline_jobs",
1303
1378
  "list_pipeline_trigger_jobs",
1304
1379
  "get_pipeline_job",
@@ -1309,11 +1384,14 @@ const TOOLSET_DEFINITIONS = [
1309
1384
  "play_pipeline_job",
1310
1385
  "retry_pipeline_job",
1311
1386
  "cancel_pipeline_job",
1387
+ "list_job_artifacts",
1388
+ "download_job_artifacts",
1389
+ "get_job_artifact_file",
1312
1390
  ]),
1313
1391
  },
1314
1392
  {
1315
1393
  id: "milestones",
1316
- isDefault: false,
1394
+ isDefault: true,
1317
1395
  tools: new Set([
1318
1396
  "list_milestones",
1319
1397
  "get_milestone",
@@ -1328,7 +1406,7 @@ const TOOLSET_DEFINITIONS = [
1328
1406
  },
1329
1407
  {
1330
1408
  id: "wiki",
1331
- isDefault: false,
1409
+ isDefault: true,
1332
1410
  tools: new Set([
1333
1411
  "list_wiki_pages",
1334
1412
  "get_wiki_page",
@@ -1440,6 +1518,7 @@ if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
1440
1518
  logger.warn("GITLAB_TOOLSETS is set alongside legacy flags (USE_PIPELINE, USE_MILESTONE, USE_GITLAB_WIKI). " +
1441
1519
  "Legacy flags add tools additively on top of the toolset selection and may produce unexpected results.");
1442
1520
  }
1521
+ const MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT = 10;
1443
1522
  /**
1444
1523
  * Smart URL handling for GitLab API
1445
1524
  *
@@ -1615,12 +1694,12 @@ async function getDefaultBranchRef(projectId) {
1615
1694
  * @returns {Promise<GitLabContent>} The file content
1616
1695
  */
1617
1696
  async function getFileContents(projectId, filePath, ref) {
1618
- projectId = decodeURIComponent(projectId); // Decode project ID
1619
- const effectiveProjectId = getEffectiveProjectId(projectId);
1697
+ const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
1698
+ const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
1620
1699
  const encodedPath = encodeURIComponent(filePath);
1621
1700
  // Fall back to default branch if ref is not provided
1622
1701
  if (!ref) {
1623
- ref = await getDefaultBranchRef(projectId);
1702
+ ref = await getDefaultBranchRef(decodedProjectId);
1624
1703
  }
1625
1704
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/files/${encodedPath}`);
1626
1705
  url.searchParams.append("ref", ref);
@@ -2454,6 +2533,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
2454
2533
  let url;
2455
2534
  if (mergeRequestIid) {
2456
2535
  url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
2536
+ url.searchParams.append("include_diverged_commits_count", "true");
2457
2537
  }
2458
2538
  else if (branchName) {
2459
2539
  url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
@@ -2468,10 +2548,219 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
2468
2548
  const data = await response.json();
2469
2549
  // If response is an array (Comes from branchName search), return the first item if exist
2470
2550
  if (Array.isArray(data) && data.length > 0) {
2471
- return GitLabMergeRequestSchema.parse(data[0]);
2551
+ const mergeRequest = GitLabMergeRequestSchema.parse(data[0]);
2552
+ return getMergeRequest(projectId, mergeRequest.iid, undefined);
2472
2553
  }
2473
2554
  return GitLabMergeRequestSchema.parse(data);
2474
2555
  }
2556
+ async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
2557
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/commits`);
2558
+ url.searchParams.append("per_page", "100");
2559
+ let totalCount = 0;
2560
+ let page = 1;
2561
+ while (true) {
2562
+ url.searchParams.set("page", String(page));
2563
+ const response = await fetch(url.toString(), {
2564
+ ...getFetchConfig(),
2565
+ });
2566
+ await handleGitLabError(response);
2567
+ const data = await response.json();
2568
+ if (!Array.isArray(data)) {
2569
+ throw new Error("Unexpected merge request commits response format");
2570
+ }
2571
+ totalCount += data.length;
2572
+ const nextPage = response.headers.get("x-next-page");
2573
+ if (!nextPage) {
2574
+ break;
2575
+ }
2576
+ page = Number.parseInt(nextPage, 10);
2577
+ if (Number.isNaN(page) || page <= 0) {
2578
+ break;
2579
+ }
2580
+ }
2581
+ return totalCount;
2582
+ }
2583
+ async function getProjectMergeMethod(projectId) {
2584
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`);
2585
+ const response = await fetch(url.toString(), {
2586
+ ...getFetchConfig(),
2587
+ });
2588
+ await handleGitLabError(response);
2589
+ const data = await response.json();
2590
+ const mergeMethod = z
2591
+ .object({
2592
+ merge_method: z.string().nullable().optional(),
2593
+ })
2594
+ .parse(data).merge_method;
2595
+ return typeof mergeMethod === "string" ? mergeMethod : null;
2596
+ }
2597
+ function estimateMergeCommitCount(mergeMethod, sourceCommitCount) {
2598
+ if (sourceCommitCount === 0) {
2599
+ return 0;
2600
+ }
2601
+ if (mergeMethod === "merge") {
2602
+ return 1;
2603
+ }
2604
+ if (mergeMethod === "ff" || mergeMethod === "rebase_merge") {
2605
+ return 0;
2606
+ }
2607
+ return null;
2608
+ }
2609
+ async function buildMergeRequestCommitAdditionSummary(projectId, mergeRequest) {
2610
+ try {
2611
+ const sourceCommitCount = await getMergeRequestSourceCommitCount(projectId, mergeRequest.iid);
2612
+ const mergeMethod = await getProjectMergeMethod(projectId);
2613
+ const mergeCommitCount = estimateMergeCommitCount(mergeMethod, sourceCommitCount);
2614
+ const summary = mergeCommitCount === null
2615
+ ? null
2616
+ : `${sourceCommitCount} commits and ${mergeCommitCount} merge commit${mergeCommitCount === 1 ? "" : "s"} will be added to ${mergeRequest.target_branch}.`;
2617
+ return {
2618
+ target_branch: mergeRequest.target_branch,
2619
+ source_commits_count: sourceCommitCount,
2620
+ merge_method: mergeMethod,
2621
+ merge_commit_count: mergeCommitCount,
2622
+ summary,
2623
+ };
2624
+ }
2625
+ catch (error) {
2626
+ const unavailableReason = error instanceof Error ? error.message : String(error);
2627
+ return {
2628
+ target_branch: mergeRequest.target_branch,
2629
+ source_commits_count: null,
2630
+ merge_method: null,
2631
+ merge_commit_count: null,
2632
+ summary: null,
2633
+ unavailable_reason: unavailableReason,
2634
+ };
2635
+ }
2636
+ }
2637
+ async function buildMergeRequestApprovalSummary(projectId, mergeRequestIid) {
2638
+ try {
2639
+ const approvalState = await getMergeRequestApprovalState(projectId, mergeRequestIid);
2640
+ const approvedByUsers = approvalState.approved_by || [];
2641
+ const approvedByUsernames = approvalState.approved_by_usernames || approvedByUsers.map(user => user.username);
2642
+ const inferredApproved = inferMergeRequestApproved(approvalState.rules);
2643
+ return {
2644
+ approved: approvalState.approved ?? inferredApproved,
2645
+ user_has_approved: approvalState.user_has_approved ?? null,
2646
+ user_can_approve: approvalState.user_can_approve ?? null,
2647
+ approved_by: approvedByUsers,
2648
+ approved_by_usernames: approvedByUsernames,
2649
+ rules_count: approvalState.rules?.length ?? null,
2650
+ source_endpoint: approvalState.source_endpoint ?? null,
2651
+ };
2652
+ }
2653
+ catch (error) {
2654
+ const unavailableReason = error instanceof Error ? error.message : String(error);
2655
+ return {
2656
+ approved: null,
2657
+ user_has_approved: null,
2658
+ user_can_approve: null,
2659
+ approved_by: [],
2660
+ approved_by_usernames: [],
2661
+ rules_count: null,
2662
+ source_endpoint: null,
2663
+ unavailable_reason: unavailableReason,
2664
+ };
2665
+ }
2666
+ }
2667
+ function toMergeRequestDeploymentSummaryRecord(deployment) {
2668
+ return {
2669
+ id: deployment.id,
2670
+ status: deployment.status,
2671
+ ref: deployment.ref,
2672
+ sha: deployment.sha,
2673
+ created_at: deployment.created_at,
2674
+ updated_at: deployment.updated_at,
2675
+ finished_at: deployment.finished_at,
2676
+ web_url: deployment.web_url,
2677
+ environment: deployment.environment
2678
+ ? {
2679
+ id: deployment.environment.id,
2680
+ name: deployment.environment.name,
2681
+ slug: deployment.environment.slug,
2682
+ external_url: deployment.environment.external_url,
2683
+ state: deployment.environment.state,
2684
+ tier: deployment.environment.tier,
2685
+ }
2686
+ : undefined,
2687
+ deployable: deployment.deployable === null
2688
+ ? null
2689
+ : deployment.deployable
2690
+ ? {
2691
+ id: deployment.deployable.id,
2692
+ name: deployment.deployable.name,
2693
+ status: deployment.deployable.status,
2694
+ stage: deployment.deployable.stage,
2695
+ web_url: deployment.deployable.web_url,
2696
+ pipeline: deployment.deployable.pipeline
2697
+ ? {
2698
+ id: deployment.deployable.pipeline.id,
2699
+ status: deployment.deployable.pipeline.status,
2700
+ ref: deployment.deployable.pipeline.ref,
2701
+ sha: deployment.deployable.pipeline.sha,
2702
+ web_url: deployment.deployable.pipeline.web_url,
2703
+ }
2704
+ : undefined,
2705
+ }
2706
+ : undefined,
2707
+ };
2708
+ }
2709
+ function sortDeploymentsByCreatedAtDesc(deployments) {
2710
+ return [...deployments].sort((a, b) => {
2711
+ const aTime = Date.parse(a.created_at);
2712
+ const bTime = Date.parse(b.created_at);
2713
+ if (Number.isNaN(aTime) || Number.isNaN(bTime)) {
2714
+ return b.created_at.localeCompare(a.created_at);
2715
+ }
2716
+ return bTime - aTime;
2717
+ });
2718
+ }
2719
+ async function buildMergeRequestDeploymentSummary(projectId, mergeRequest) {
2720
+ const lookupSha = mergeRequest.merge_commit_sha ?? mergeRequest.diff_refs?.head_sha ?? null;
2721
+ if (!lookupSha) {
2722
+ return {
2723
+ lookup_sha: null,
2724
+ sort: "created_at_desc",
2725
+ limit: MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT,
2726
+ total_count: 0,
2727
+ returned_count: 0,
2728
+ records: [],
2729
+ };
2730
+ }
2731
+ try {
2732
+ const deployments = await listDeployments(projectId, {
2733
+ sha: lookupSha,
2734
+ order_by: "created_at",
2735
+ sort: "desc",
2736
+ per_page: 100,
2737
+ });
2738
+ const sortedDeployments = sortDeploymentsByCreatedAtDesc(deployments);
2739
+ const records = sortedDeployments
2740
+ .slice(0, MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT)
2741
+ .map(toMergeRequestDeploymentSummaryRecord);
2742
+ return {
2743
+ lookup_sha: lookupSha,
2744
+ sort: "created_at_desc",
2745
+ limit: MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT,
2746
+ total_count: sortedDeployments.length,
2747
+ returned_count: records.length,
2748
+ records,
2749
+ };
2750
+ }
2751
+ catch (error) {
2752
+ const unavailableReason = error instanceof Error ? error.message : String(error);
2753
+ return {
2754
+ lookup_sha: lookupSha,
2755
+ sort: "created_at_desc",
2756
+ limit: MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT,
2757
+ total_count: 0,
2758
+ returned_count: 0,
2759
+ records: [],
2760
+ unavailable_reason: unavailableReason,
2761
+ };
2762
+ }
2763
+ }
2475
2764
  /**
2476
2765
  * Get merge request changes/diffs
2477
2766
  * MR 변경사항 조회 함수 (Function to retrieve merge request changes)
@@ -2666,13 +2955,61 @@ async function unapproveMergeRequest(projectId, mergeRequestIid) {
2666
2955
  */
2667
2956
  async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
2668
2957
  projectId = decodeURIComponent(projectId);
2669
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approval_state`);
2670
- const response = await fetch(url.toString(), {
2958
+ const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approval_state`);
2959
+ const approvalStateResponse = await fetch(approvalStateUrl.toString(), {
2671
2960
  ...getFetchConfig(),
2672
2961
  method: "GET",
2673
2962
  });
2674
- await handleGitLabError(response);
2675
- return GitLabMergeRequestApprovalStateSchema.parse(await response.json());
2963
+ if (approvalStateResponse.status === 404) {
2964
+ return getMergeRequestApprovalsFallback(projectId, mergeRequestIid);
2965
+ }
2966
+ await handleGitLabError(approvalStateResponse);
2967
+ const parsedApprovalState = GitLabMergeRequestApprovalStateSchema.parse(await approvalStateResponse.json());
2968
+ const approvedByUsers = getUniqueApprovalUsers((parsedApprovalState.rules || []).flatMap(rule => rule.approved_by || []));
2969
+ const approvedByUsernames = approvedByUsers.map(user => user.username);
2970
+ return {
2971
+ ...parsedApprovalState,
2972
+ approved_by: approvedByUsers,
2973
+ approved_by_usernames: approvedByUsernames,
2974
+ source_endpoint: "approval_state",
2975
+ };
2976
+ }
2977
+ async function getMergeRequestApprovalsFallback(projectId, mergeRequestIid) {
2978
+ const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approvals`);
2979
+ const approvalsResponse = await fetch(approvalsUrl.toString(), {
2980
+ ...getFetchConfig(),
2981
+ method: "GET",
2982
+ });
2983
+ await handleGitLabError(approvalsResponse);
2984
+ const parsedApprovals = GitLabMergeRequestApprovalsResponseSchema.parse(await approvalsResponse.json());
2985
+ const approvedByUsers = getUniqueApprovalUsers((parsedApprovals.approved_by || []).map(approvedByEntry => approvedByEntry.user));
2986
+ const approvedByUsernames = approvedByUsers.map(user => user.username);
2987
+ return GitLabMergeRequestApprovalStateSchema.parse({
2988
+ approved: parsedApprovals.approved,
2989
+ user_has_approved: parsedApprovals.user_has_approved,
2990
+ user_can_approve: parsedApprovals.user_can_approve,
2991
+ approved_by: approvedByUsers,
2992
+ approved_by_usernames: approvedByUsernames,
2993
+ source_endpoint: "approvals",
2994
+ });
2995
+ }
2996
+ function getUniqueApprovalUsers(users) {
2997
+ const uniqueUsers = new Map();
2998
+ for (const user of users) {
2999
+ if (!uniqueUsers.has(user.id)) {
3000
+ uniqueUsers.set(user.id, user);
3001
+ }
3002
+ }
3003
+ return [...uniqueUsers.values()];
3004
+ }
3005
+ function inferMergeRequestApproved(rules) {
3006
+ if (!rules || rules.length === 0) {
3007
+ return null;
3008
+ }
3009
+ if (rules.some(rule => typeof rule.approved !== "boolean")) {
3010
+ return null;
3011
+ }
3012
+ return rules.every(rule => rule.approved === true);
2676
3013
  }
2677
3014
  /**
2678
3015
  * Create a new note (comment) on an issue or merge request
@@ -3409,6 +3746,90 @@ async function getPipeline(projectId, pipelineId) {
3409
3746
  const data = await response.json();
3410
3747
  return GitLabPipelineSchema.parse(data);
3411
3748
  }
3749
+ /**
3750
+ * List deployments in a GitLab project
3751
+ *
3752
+ * @param {string} projectId - The ID or URL-encoded path of the project
3753
+ * @param {ListDeploymentsOptions} options - Options for filtering deployments
3754
+ * @returns {Promise<GitLabDeployment[]>} List of deployments
3755
+ */
3756
+ async function listDeployments(projectId, options = {}) {
3757
+ projectId = decodeURIComponent(projectId);
3758
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments`);
3759
+ Object.entries(options).forEach(([key, value]) => {
3760
+ if (value !== undefined) {
3761
+ url.searchParams.append(key, value.toString());
3762
+ }
3763
+ });
3764
+ const response = await fetch(url.toString(), {
3765
+ ...getFetchConfig(),
3766
+ });
3767
+ await handleGitLabError(response);
3768
+ const data = await response.json();
3769
+ return z.array(GitLabDeploymentSchema).parse(data);
3770
+ }
3771
+ /**
3772
+ * Get details of a specific deployment
3773
+ *
3774
+ * @param {string} projectId - The ID or URL-encoded path of the project
3775
+ * @param {number | string} deploymentId - The ID of the deployment
3776
+ * @returns {Promise<GitLabDeployment>} Deployment details
3777
+ */
3778
+ async function getDeployment(projectId, deploymentId) {
3779
+ projectId = decodeURIComponent(projectId);
3780
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments/${deploymentId}`);
3781
+ const response = await fetch(url.toString(), {
3782
+ ...getFetchConfig(),
3783
+ });
3784
+ if (response.status === 404) {
3785
+ throw new Error(`Deployment not found`);
3786
+ }
3787
+ await handleGitLabError(response);
3788
+ const data = await response.json();
3789
+ return GitLabDeploymentSchema.parse(data);
3790
+ }
3791
+ /**
3792
+ * List environments in a GitLab project
3793
+ *
3794
+ * @param {string} projectId - The ID or URL-encoded path of the project
3795
+ * @param {ListEnvironmentsOptions} options - Options for filtering environments
3796
+ * @returns {Promise<GitLabEnvironment[]>} List of environments
3797
+ */
3798
+ async function listEnvironments(projectId, options = {}) {
3799
+ projectId = decodeURIComponent(projectId);
3800
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments`);
3801
+ Object.entries(options).forEach(([key, value]) => {
3802
+ if (value !== undefined) {
3803
+ url.searchParams.append(key, value.toString());
3804
+ }
3805
+ });
3806
+ const response = await fetch(url.toString(), {
3807
+ ...getFetchConfig(),
3808
+ });
3809
+ await handleGitLabError(response);
3810
+ const data = await response.json();
3811
+ return z.array(GitLabEnvironmentSchema).parse(data);
3812
+ }
3813
+ /**
3814
+ * Get details of a specific environment
3815
+ *
3816
+ * @param {string} projectId - The ID or URL-encoded path of the project
3817
+ * @param {number | string} environmentId - The ID of the environment
3818
+ * @returns {Promise<GitLabEnvironment>} Environment details
3819
+ */
3820
+ async function getEnvironment(projectId, environmentId) {
3821
+ projectId = decodeURIComponent(projectId);
3822
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments/${environmentId}`);
3823
+ const response = await fetch(url.toString(), {
3824
+ ...getFetchConfig(),
3825
+ });
3826
+ if (response.status === 404) {
3827
+ throw new Error(`Environment not found`);
3828
+ }
3829
+ await handleGitLabError(response);
3830
+ const data = await response.json();
3831
+ return GitLabEnvironmentSchema.parse(data);
3832
+ }
3412
3833
  /**
3413
3834
  * List all jobs in a specific pipeline
3414
3835
  *
@@ -3533,21 +3954,107 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
3533
3954
  }
3534
3955
  return fullTrace;
3535
3956
  }
3957
+ /**
3958
+ * List artifact files in a job's artifacts archive
3959
+ *
3960
+ * @param {string} projectId - The ID or URL-encoded path of the project
3961
+ * @param {string} jobId - The ID of the job
3962
+ * @param {Object} options - Options for listing artifacts
3963
+ * @returns {Promise<GitLabArtifactEntry[]>} List of artifact entries
3964
+ */
3965
+ async function listJobArtifacts(projectId, jobId, options = {}) {
3966
+ projectId = decodeURIComponent(projectId);
3967
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/artifacts/tree`);
3968
+ Object.entries(options).forEach(([key, value]) => {
3969
+ if (value !== undefined) {
3970
+ if (typeof value === "boolean") {
3971
+ url.searchParams.append(key, value ? "true" : "false");
3972
+ }
3973
+ else {
3974
+ url.searchParams.append(key, value.toString());
3975
+ }
3976
+ }
3977
+ });
3978
+ const response = await fetch(url.toString(), {
3979
+ ...getFetchConfig(),
3980
+ });
3981
+ if (response.status === 404) {
3982
+ throw new Error(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
3983
+ }
3984
+ await handleGitLabError(response);
3985
+ const data = await response.json();
3986
+ return z.array(GitLabArtifactEntrySchema).parse(data);
3987
+ }
3988
+ /**
3989
+ * Download the entire artifact archive for a job and save to disk
3990
+ *
3991
+ * @param {string} projectId - The ID or URL-encoded path of the project
3992
+ * @param {string} jobId - The ID of the job
3993
+ * @param {string} localPath - Optional local directory to save the archive
3994
+ * @returns {Promise<string>} The path where the artifact archive was saved
3995
+ */
3996
+ async function downloadJobArtifacts(projectId, jobId, localPath) {
3997
+ projectId = decodeURIComponent(projectId);
3998
+ const effectiveProjectId = getEffectiveProjectId(projectId);
3999
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${jobId}/artifacts`);
4000
+ const response = await fetch(url.toString(), {
4001
+ ...getFetchConfig(),
4002
+ });
4003
+ if (response.status === 404) {
4004
+ throw new Error(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
4005
+ }
4006
+ await handleGitLabError(response);
4007
+ const buffer = await response.arrayBuffer();
4008
+ const filename = `artifacts_job_${jobId}.zip`;
4009
+ const savePath = localPath ? path.join(localPath, filename) : filename;
4010
+ fs.mkdirSync(path.dirname(savePath), { recursive: true });
4011
+ fs.writeFileSync(savePath, Buffer.from(buffer));
4012
+ return savePath;
4013
+ }
4014
+ /**
4015
+ * Download a single file from a job's artifacts
4016
+ *
4017
+ * @param {string} projectId - The ID or URL-encoded path of the project
4018
+ * @param {string} jobId - The ID of the job
4019
+ * @param {string} artifactPath - Path to the file within the artifacts archive
4020
+ * @returns {Promise<string>} The file content as text
4021
+ */
4022
+ async function getJobArtifactFile(projectId, jobId, artifactPath) {
4023
+ projectId = decodeURIComponent(projectId);
4024
+ const effectiveProjectId = getEffectiveProjectId(projectId);
4025
+ const encodedArtifactPath = artifactPath
4026
+ .split("/")
4027
+ .map(segment => encodeURIComponent(segment))
4028
+ .join("/");
4029
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${jobId}/artifacts/${encodedArtifactPath}`);
4030
+ const response = await fetch(url.toString(), {
4031
+ ...getFetchConfig(),
4032
+ });
4033
+ if (response.status === 404) {
4034
+ throw new Error(`Artifact file not found: ${artifactPath}`);
4035
+ }
4036
+ await handleGitLabError(response);
4037
+ return await response.text();
4038
+ }
3536
4039
  /**
3537
4040
  * Create a new pipeline
3538
4041
  *
3539
4042
  * @param {string} projectId - The ID or URL-encoded path of the project
3540
4043
  * @param {string} ref - The branch or tag to run the pipeline on
3541
4044
  * @param {Array} variables - Optional variables for the pipeline
4045
+ * @param {Record<string, string>} inputs - Optional input parameters for the pipeline
3542
4046
  * @returns {Promise<GitLabPipeline>} The created pipeline
3543
4047
  */
3544
- async function createPipeline(projectId, ref, variables) {
4048
+ async function createPipeline(projectId, ref, variables, inputs) {
3545
4049
  projectId = decodeURIComponent(projectId); // Decode project ID
3546
4050
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline`);
3547
4051
  const body = { ref };
3548
4052
  if (variables && variables.length > 0) {
3549
4053
  body.variables = variables;
3550
4054
  }
4055
+ if (inputs && Object.keys(inputs).length > 0) {
4056
+ body.inputs = inputs;
4057
+ }
3551
4058
  const response = await fetch(url.toString(), {
3552
4059
  method: "POST",
3553
4060
  headers: {
@@ -4450,6 +4957,8 @@ async function handleToolCall(params) {
4450
4957
  if (GITLAB_AUTH_COOKIE_PATH) {
4451
4958
  await ensureSessionForRequest();
4452
4959
  }
4960
+ // Lazy OAuth token refresh: only validate/refresh when a tool is actually called
4961
+ await ensureValidOAuthToken();
4453
4962
  logger.info(params.name);
4454
4963
  switch (params.name) {
4455
4964
  case "execute_graphql": {
@@ -4678,8 +5187,22 @@ async function handleToolCall(params) {
4678
5187
  case "get_merge_request": {
4679
5188
  const args = GetMergeRequestSchema.parse(params.arguments);
4680
5189
  const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid, args.source_branch);
5190
+ const deploymentSummary = await buildMergeRequestDeploymentSummary(args.project_id, mergeRequest);
5191
+ const commitAdditionSummary = await buildMergeRequestCommitAdditionSummary(args.project_id, mergeRequest);
5192
+ const approvalSummary = await buildMergeRequestApprovalSummary(args.project_id, mergeRequest.iid);
5193
+ const mergeRequestWithDeploymentSummary = {
5194
+ ...mergeRequest,
5195
+ deployment_summary: deploymentSummary,
5196
+ commit_addition_summary: commitAdditionSummary,
5197
+ approval_summary: approvalSummary,
5198
+ };
4681
5199
  return {
4682
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
5200
+ content: [
5201
+ {
5202
+ type: "text",
5203
+ text: JSON.stringify(mergeRequestWithDeploymentSummary, null, 2),
5204
+ },
5205
+ ],
4683
5206
  };
4684
5207
  }
4685
5208
  case "get_merge_request_diffs": {
@@ -5138,6 +5661,36 @@ async function handleToolCall(params) {
5138
5661
  ],
5139
5662
  };
5140
5663
  }
5664
+ case "list_deployments": {
5665
+ const args = ListDeploymentsSchema.parse(params.arguments);
5666
+ const { project_id, ...options } = args;
5667
+ const deployments = await listDeployments(project_id, options);
5668
+ return {
5669
+ content: [{ type: "text", text: JSON.stringify(deployments, null, 2) }],
5670
+ };
5671
+ }
5672
+ case "get_deployment": {
5673
+ const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
5674
+ const deployment = await getDeployment(project_id, deployment_id);
5675
+ return {
5676
+ content: [{ type: "text", text: JSON.stringify(deployment, null, 2) }],
5677
+ };
5678
+ }
5679
+ case "list_environments": {
5680
+ const args = ListEnvironmentsSchema.parse(params.arguments);
5681
+ const { project_id, ...options } = args;
5682
+ const environments = await listEnvironments(project_id, options);
5683
+ return {
5684
+ content: [{ type: "text", text: JSON.stringify(environments, null, 2) }],
5685
+ };
5686
+ }
5687
+ case "get_environment": {
5688
+ const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
5689
+ const environment = await getEnvironment(project_id, environment_id);
5690
+ return {
5691
+ content: [{ type: "text", text: JSON.stringify(environment, null, 2) }],
5692
+ };
5693
+ }
5141
5694
  case "list_pipeline_jobs": {
5142
5695
  const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(params.arguments);
5143
5696
  const jobs = await listPipelineJobs(project_id, pipeline_id, options);
@@ -5187,8 +5740,8 @@ async function handleToolCall(params) {
5187
5740
  };
5188
5741
  }
5189
5742
  case "create_pipeline": {
5190
- const { project_id, ref, variables } = CreatePipelineSchema.parse(params.arguments);
5191
- const pipeline = await createPipeline(project_id, ref, variables);
5743
+ const { project_id, ref, variables, inputs } = CreatePipelineSchema.parse(params.arguments);
5744
+ const pipeline = await createPipeline(project_id, ref, variables, inputs);
5192
5745
  return {
5193
5746
  content: [
5194
5747
  {
@@ -5258,6 +5811,42 @@ async function handleToolCall(params) {
5258
5811
  ],
5259
5812
  };
5260
5813
  }
5814
+ case "list_job_artifacts": {
5815
+ const { project_id, job_id, ...options } = ListJobArtifactsSchema.parse(params.arguments);
5816
+ const artifacts = await listJobArtifacts(project_id, job_id, options);
5817
+ return {
5818
+ content: [
5819
+ {
5820
+ type: "text",
5821
+ text: JSON.stringify(artifacts, null, 2),
5822
+ },
5823
+ ],
5824
+ };
5825
+ }
5826
+ case "download_job_artifacts": {
5827
+ const { project_id, job_id, local_path } = DownloadJobArtifactsSchema.parse(params.arguments);
5828
+ const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
5829
+ return {
5830
+ content: [
5831
+ {
5832
+ type: "text",
5833
+ text: JSON.stringify({ success: true, file_path: filePath }, null, 2),
5834
+ },
5835
+ ],
5836
+ };
5837
+ }
5838
+ case "get_job_artifact_file": {
5839
+ const { project_id, job_id, artifact_path } = GetJobArtifactFileSchema.parse(params.arguments);
5840
+ const fileContent = await getJobArtifactFile(project_id, job_id, artifact_path);
5841
+ return {
5842
+ content: [
5843
+ {
5844
+ type: "text",
5845
+ text: fileContent,
5846
+ },
5847
+ ],
5848
+ };
5849
+ }
5261
5850
  case "list_merge_requests": {
5262
5851
  const args = ListMergeRequestsSchema.parse(params.arguments);
5263
5852
  const mergeRequests = await listMergeRequests(args.project_id, args);
@@ -6004,9 +6593,10 @@ async function runServer() {
6004
6593
  logger.info("Using OAuth authentication...");
6005
6594
  try {
6006
6595
  const gitlabBaseUrl = GITLAB_API_URL.replace(/\/api\/v4$/, "");
6007
- OAUTH_ACCESS_TOKEN = await initializeOAuth(gitlabBaseUrl);
6596
+ const oauthResult = await initializeOAuthClient(gitlabBaseUrl);
6597
+ oauthClient = oauthResult.client;
6598
+ OAUTH_ACCESS_TOKEN = oauthResult.accessToken;
6008
6599
  logger.info("OAuth authentication successful");
6009
- // Note: Headers are automatically generated by buildAuthHeaders() using OAUTH_ACCESS_TOKEN
6010
6600
  }
6011
6601
  catch (error) {
6012
6602
  logger.error("OAuth authentication failed:", error);