@zereight/mcp-gitlab 2.0.30 → 2.0.33
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 +8 -8
- package/build/index.js +777 -28
- package/build/oauth.js +16 -4
- package/build/schemas.js +310 -12
- package/build/test/schema-tests.js +311 -0
- package/build/test/test-deployment-tools.js +366 -0
- package/build/test/test-job-artifacts.js +194 -0
- package/build/test/test-merge-request-approval-state-tools.js +171 -0
- package/build/test/test-toolset-filtering.js +7 -6
- package/package.json +3 -2
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 {
|
|
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, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, } from "./schemas.js";
|
|
54
54
|
import { randomUUID } from "node:crypto";
|
|
55
55
|
import { pino } from "pino";
|
|
56
56
|
const logger = pino({
|
|
@@ -226,9 +226,10 @@ function validateConfiguration() {
|
|
|
226
226
|
const remoteAuth = getConfig("remote-auth", "REMOTE_AUTHORIZATION") === "true";
|
|
227
227
|
const useOAuth = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
|
|
228
228
|
const hasToken = !!getConfig("token", "GITLAB_PERSONAL_ACCESS_TOKEN");
|
|
229
|
+
const hasJobToken = !!getConfig("job-token", "GITLAB_JOB_TOKEN");
|
|
229
230
|
const hasCookie = !!getConfig("cookie-path", "GITLAB_AUTH_COOKIE_PATH");
|
|
230
|
-
if (!remoteAuth && !useOAuth && !hasToken && !hasCookie) {
|
|
231
|
-
errors.push("Either --token, --cookie-path, --use-oauth=true, or --remote-auth=true must be set (or use environment variables)");
|
|
231
|
+
if (!remoteAuth && !useOAuth && !hasToken && !hasJobToken && !hasCookie) {
|
|
232
|
+
errors.push("Either --token, --job-token, --cookie-path, --use-oauth=true, or --remote-auth=true must be set (or use environment variables)");
|
|
232
233
|
}
|
|
233
234
|
const enableDynamicApiUrl = getConfig("enable-dynamic-api-url", "ENABLE_DYNAMIC_API_URL") === "true";
|
|
234
235
|
if (enableDynamicApiUrl && !remoteAuth) {
|
|
@@ -242,7 +243,30 @@ function validateConfiguration() {
|
|
|
242
243
|
logger.info("Configuration validation passed");
|
|
243
244
|
}
|
|
244
245
|
const GITLAB_PERSONAL_ACCESS_TOKEN = getConfig("token", "GITLAB_PERSONAL_ACCESS_TOKEN");
|
|
246
|
+
const GITLAB_JOB_TOKEN = getConfig("job-token", "GITLAB_JOB_TOKEN");
|
|
245
247
|
let OAUTH_ACCESS_TOKEN = null;
|
|
248
|
+
let oauthClient = null;
|
|
249
|
+
/**
|
|
250
|
+
* Ensure the OAuth token is valid before making an API call.
|
|
251
|
+
* Refreshes the token lazily (only when a tool is actually called).
|
|
252
|
+
* This avoids background timers that cause issues with multiple instances.
|
|
253
|
+
*/
|
|
254
|
+
async function ensureValidOAuthToken() {
|
|
255
|
+
if (!oauthClient)
|
|
256
|
+
return;
|
|
257
|
+
if (oauthClient.hasValidToken())
|
|
258
|
+
return;
|
|
259
|
+
try {
|
|
260
|
+
logger.info("OAuth token expired or missing, refreshing...");
|
|
261
|
+
const freshToken = await oauthClient.getAccessToken();
|
|
262
|
+
OAUTH_ACCESS_TOKEN = freshToken;
|
|
263
|
+
logger.info("OAuth token refreshed successfully");
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
logger.error("Failed to refresh OAuth token:", error);
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
246
270
|
const GITLAB_AUTH_COOKIE_PATH = getConfig("cookie-path", "GITLAB_AUTH_COOKIE_PATH");
|
|
247
271
|
const USE_OAUTH = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
|
|
248
272
|
const IS_OLD = getConfig("is-old", "GITLAB_IS_OLD") === "true";
|
|
@@ -457,7 +481,7 @@ const BASE_HEADERS = {
|
|
|
457
481
|
/**
|
|
458
482
|
* Build authentication headers dynamically based on context
|
|
459
483
|
* In REMOTE_AUTHORIZATION mode, reads from AsyncLocalStorage session context
|
|
460
|
-
* Otherwise, uses environment token
|
|
484
|
+
* Otherwise, uses environment token (OAuth token is refreshed lazily before each tool call)
|
|
461
485
|
*/
|
|
462
486
|
function buildAuthHeaders() {
|
|
463
487
|
if (REMOTE_AUTHORIZATION) {
|
|
@@ -470,6 +494,10 @@ function buildAuthHeaders() {
|
|
|
470
494
|
}
|
|
471
495
|
return {}; // No auth headers if no session context
|
|
472
496
|
}
|
|
497
|
+
// CI job tokens use a dedicated header (not Bearer/Private-Token)
|
|
498
|
+
if (GITLAB_JOB_TOKEN) {
|
|
499
|
+
return { "JOB-TOKEN": String(GITLAB_JOB_TOKEN) };
|
|
500
|
+
}
|
|
473
501
|
// Standard mode: prioritize OAuth token, then fall back to environment token
|
|
474
502
|
const token = OAUTH_ACCESS_TOKEN || GITLAB_PERSONAL_ACCESS_TOKEN;
|
|
475
503
|
if (IS_OLD && token) {
|
|
@@ -572,7 +600,7 @@ const allTools = [
|
|
|
572
600
|
},
|
|
573
601
|
{
|
|
574
602
|
name: "get_merge_request_approval_state",
|
|
575
|
-
description: "Get
|
|
603
|
+
description: "Get merge request approval details including approvers (uses approval_state when available, falls back to approvals endpoint)",
|
|
576
604
|
inputSchema: toJSONSchema(GetMergeRequestApprovalStateSchema),
|
|
577
605
|
},
|
|
578
606
|
{
|
|
@@ -627,7 +655,7 @@ const allTools = [
|
|
|
627
655
|
},
|
|
628
656
|
{
|
|
629
657
|
name: "get_merge_request",
|
|
630
|
-
description: "Get details of a merge request (Either mergeRequestIid or branchName must be provided)",
|
|
658
|
+
description: "Get details of a merge request with compact deployment, commit addition, and approval summaries (Either mergeRequestIid or branchName must be provided)",
|
|
631
659
|
inputSchema: toJSONSchema(GetMergeRequestSchema),
|
|
632
660
|
},
|
|
633
661
|
{
|
|
@@ -915,6 +943,26 @@ const allTools = [
|
|
|
915
943
|
description: "Get details of a specific pipeline in a GitLab project",
|
|
916
944
|
inputSchema: toJSONSchema(GetPipelineSchema),
|
|
917
945
|
},
|
|
946
|
+
{
|
|
947
|
+
name: "list_deployments",
|
|
948
|
+
description: "List deployments in a GitLab project with filtering options",
|
|
949
|
+
inputSchema: toJSONSchema(ListDeploymentsSchema),
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
name: "get_deployment",
|
|
953
|
+
description: "Get details of a specific deployment in a GitLab project",
|
|
954
|
+
inputSchema: toJSONSchema(GetDeploymentSchema),
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
name: "list_environments",
|
|
958
|
+
description: "List environments in a GitLab project",
|
|
959
|
+
inputSchema: toJSONSchema(ListEnvironmentsSchema),
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
name: "get_environment",
|
|
963
|
+
description: "Get details of a specific environment in a GitLab project",
|
|
964
|
+
inputSchema: toJSONSchema(GetEnvironmentSchema),
|
|
965
|
+
},
|
|
918
966
|
{
|
|
919
967
|
name: "list_pipeline_jobs",
|
|
920
968
|
description: "List all jobs in a specific pipeline",
|
|
@@ -965,6 +1013,21 @@ const allTools = [
|
|
|
965
1013
|
description: "Cancel a running pipeline job",
|
|
966
1014
|
inputSchema: toJSONSchema(CancelPipelineJobSchema),
|
|
967
1015
|
},
|
|
1016
|
+
{
|
|
1017
|
+
name: "list_job_artifacts",
|
|
1018
|
+
description: "List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes.",
|
|
1019
|
+
inputSchema: toJSONSchema(ListJobArtifactsSchema),
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
name: "download_job_artifacts",
|
|
1023
|
+
description: "Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path.",
|
|
1024
|
+
inputSchema: toJSONSchema(DownloadJobArtifactsSchema),
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "get_job_artifact_file",
|
|
1028
|
+
description: "Get the content of a single file from a job's artifacts by its path within the archive",
|
|
1029
|
+
inputSchema: toJSONSchema(GetJobArtifactFileSchema),
|
|
1030
|
+
},
|
|
968
1031
|
{
|
|
969
1032
|
name: "list_merge_requests",
|
|
970
1033
|
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.",
|
|
@@ -1095,6 +1158,21 @@ const allTools = [
|
|
|
1095
1158
|
description: "Download a release asset file by direct asset path",
|
|
1096
1159
|
inputSchema: toJSONSchema(DownloadReleaseAssetSchema),
|
|
1097
1160
|
},
|
|
1161
|
+
{
|
|
1162
|
+
name: "list_webhooks",
|
|
1163
|
+
description: "List all configured webhooks for a GitLab project or group. Provide either project_id or group_id.",
|
|
1164
|
+
inputSchema: toJSONSchema(ListWebhooksSchema),
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
name: "list_webhook_events",
|
|
1168
|
+
description: "List recent webhook events (past 7 days) for a project or group webhook. Use summary mode for overview, then get_webhook_event for full details.",
|
|
1169
|
+
inputSchema: toJSONSchema(ListWebhookEventsSchema),
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
name: "get_webhook_event",
|
|
1173
|
+
description: "Get full details of a specific webhook event by ID, including request/response payloads. Searches up to 500 most recent events.",
|
|
1174
|
+
inputSchema: toJSONSchema(GetWebhookEventSchema),
|
|
1175
|
+
},
|
|
1098
1176
|
];
|
|
1099
1177
|
// Define which tools are read-only
|
|
1100
1178
|
const readOnlyTools = new Set([
|
|
@@ -1122,10 +1200,17 @@ const readOnlyTools = new Set([
|
|
|
1122
1200
|
"list_project_members",
|
|
1123
1201
|
"get_pipeline",
|
|
1124
1202
|
"list_pipelines",
|
|
1203
|
+
"list_deployments",
|
|
1204
|
+
"get_deployment",
|
|
1205
|
+
"list_environments",
|
|
1206
|
+
"get_environment",
|
|
1125
1207
|
"list_pipeline_jobs",
|
|
1126
1208
|
"list_pipeline_trigger_jobs",
|
|
1127
1209
|
"get_pipeline_job",
|
|
1128
1210
|
"get_pipeline_job_output",
|
|
1211
|
+
"list_job_artifacts",
|
|
1212
|
+
"download_job_artifacts",
|
|
1213
|
+
"get_job_artifact_file",
|
|
1129
1214
|
"list_labels",
|
|
1130
1215
|
"get_label",
|
|
1131
1216
|
"list_group_projects",
|
|
@@ -1150,6 +1235,9 @@ const readOnlyTools = new Set([
|
|
|
1150
1235
|
"get_release",
|
|
1151
1236
|
"download_release_asset",
|
|
1152
1237
|
"get_merge_request_approval_state",
|
|
1238
|
+
"list_webhooks",
|
|
1239
|
+
"list_webhook_events",
|
|
1240
|
+
"get_webhook_event",
|
|
1153
1241
|
]);
|
|
1154
1242
|
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
|
1155
1243
|
const wikiToolNames = new Set([
|
|
@@ -1176,6 +1264,10 @@ const milestoneToolNames = new Set([
|
|
|
1176
1264
|
const pipelineToolNames = new Set([
|
|
1177
1265
|
"list_pipelines",
|
|
1178
1266
|
"get_pipeline",
|
|
1267
|
+
"list_deployments",
|
|
1268
|
+
"get_deployment",
|
|
1269
|
+
"list_environments",
|
|
1270
|
+
"get_environment",
|
|
1179
1271
|
"list_pipeline_jobs",
|
|
1180
1272
|
"list_pipeline_trigger_jobs",
|
|
1181
1273
|
"get_pipeline_job",
|
|
@@ -1186,6 +1278,9 @@ const pipelineToolNames = new Set([
|
|
|
1186
1278
|
"play_pipeline_job",
|
|
1187
1279
|
"retry_pipeline_job",
|
|
1188
1280
|
"cancel_pipeline_job",
|
|
1281
|
+
"list_job_artifacts",
|
|
1282
|
+
"download_job_artifacts",
|
|
1283
|
+
"get_job_artifact_file",
|
|
1189
1284
|
]);
|
|
1190
1285
|
const TOOLSET_DEFINITIONS = [
|
|
1191
1286
|
{
|
|
@@ -1295,10 +1390,14 @@ const TOOLSET_DEFINITIONS = [
|
|
|
1295
1390
|
},
|
|
1296
1391
|
{
|
|
1297
1392
|
id: "pipelines",
|
|
1298
|
-
isDefault:
|
|
1393
|
+
isDefault: true,
|
|
1299
1394
|
tools: new Set([
|
|
1300
1395
|
"list_pipelines",
|
|
1301
1396
|
"get_pipeline",
|
|
1397
|
+
"list_deployments",
|
|
1398
|
+
"get_deployment",
|
|
1399
|
+
"list_environments",
|
|
1400
|
+
"get_environment",
|
|
1302
1401
|
"list_pipeline_jobs",
|
|
1303
1402
|
"list_pipeline_trigger_jobs",
|
|
1304
1403
|
"get_pipeline_job",
|
|
@@ -1309,11 +1408,14 @@ const TOOLSET_DEFINITIONS = [
|
|
|
1309
1408
|
"play_pipeline_job",
|
|
1310
1409
|
"retry_pipeline_job",
|
|
1311
1410
|
"cancel_pipeline_job",
|
|
1411
|
+
"list_job_artifacts",
|
|
1412
|
+
"download_job_artifacts",
|
|
1413
|
+
"get_job_artifact_file",
|
|
1312
1414
|
]),
|
|
1313
1415
|
},
|
|
1314
1416
|
{
|
|
1315
1417
|
id: "milestones",
|
|
1316
|
-
isDefault:
|
|
1418
|
+
isDefault: true,
|
|
1317
1419
|
tools: new Set([
|
|
1318
1420
|
"list_milestones",
|
|
1319
1421
|
"get_milestone",
|
|
@@ -1328,7 +1430,7 @@ const TOOLSET_DEFINITIONS = [
|
|
|
1328
1430
|
},
|
|
1329
1431
|
{
|
|
1330
1432
|
id: "wiki",
|
|
1331
|
-
isDefault:
|
|
1433
|
+
isDefault: true,
|
|
1332
1434
|
tools: new Set([
|
|
1333
1435
|
"list_wiki_pages",
|
|
1334
1436
|
"get_wiki_page",
|
|
@@ -1361,6 +1463,15 @@ const TOOLSET_DEFINITIONS = [
|
|
|
1361
1463
|
"download_attachment",
|
|
1362
1464
|
]),
|
|
1363
1465
|
},
|
|
1466
|
+
{
|
|
1467
|
+
id: "webhooks",
|
|
1468
|
+
isDefault: false,
|
|
1469
|
+
tools: new Set([
|
|
1470
|
+
"list_webhooks",
|
|
1471
|
+
"list_webhook_events",
|
|
1472
|
+
"get_webhook_event",
|
|
1473
|
+
]),
|
|
1474
|
+
},
|
|
1364
1475
|
];
|
|
1365
1476
|
// Derived lookup: tool name → toolset ID
|
|
1366
1477
|
const TOOLSET_BY_TOOL_NAME = new Map();
|
|
@@ -1440,6 +1551,7 @@ if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
|
|
|
1440
1551
|
logger.warn("GITLAB_TOOLSETS is set alongside legacy flags (USE_PIPELINE, USE_MILESTONE, USE_GITLAB_WIKI). " +
|
|
1441
1552
|
"Legacy flags add tools additively on top of the toolset selection and may produce unexpected results.");
|
|
1442
1553
|
}
|
|
1554
|
+
const MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT = 10;
|
|
1443
1555
|
/**
|
|
1444
1556
|
* Smart URL handling for GitLab API
|
|
1445
1557
|
*
|
|
@@ -1615,12 +1727,12 @@ async function getDefaultBranchRef(projectId) {
|
|
|
1615
1727
|
* @returns {Promise<GitLabContent>} The file content
|
|
1616
1728
|
*/
|
|
1617
1729
|
async function getFileContents(projectId, filePath, ref) {
|
|
1618
|
-
|
|
1619
|
-
const effectiveProjectId = getEffectiveProjectId(
|
|
1730
|
+
const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
|
|
1731
|
+
const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
|
|
1620
1732
|
const encodedPath = encodeURIComponent(filePath);
|
|
1621
1733
|
// Fall back to default branch if ref is not provided
|
|
1622
1734
|
if (!ref) {
|
|
1623
|
-
ref = await getDefaultBranchRef(
|
|
1735
|
+
ref = await getDefaultBranchRef(decodedProjectId);
|
|
1624
1736
|
}
|
|
1625
1737
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/files/${encodedPath}`);
|
|
1626
1738
|
url.searchParams.append("ref", ref);
|
|
@@ -2454,6 +2566,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
|
|
|
2454
2566
|
let url;
|
|
2455
2567
|
if (mergeRequestIid) {
|
|
2456
2568
|
url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
|
|
2569
|
+
url.searchParams.append("include_diverged_commits_count", "true");
|
|
2457
2570
|
}
|
|
2458
2571
|
else if (branchName) {
|
|
2459
2572
|
url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
|
|
@@ -2468,10 +2581,219 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
|
|
|
2468
2581
|
const data = await response.json();
|
|
2469
2582
|
// If response is an array (Comes from branchName search), return the first item if exist
|
|
2470
2583
|
if (Array.isArray(data) && data.length > 0) {
|
|
2471
|
-
|
|
2584
|
+
const mergeRequest = GitLabMergeRequestSchema.parse(data[0]);
|
|
2585
|
+
return getMergeRequest(projectId, mergeRequest.iid, undefined);
|
|
2472
2586
|
}
|
|
2473
2587
|
return GitLabMergeRequestSchema.parse(data);
|
|
2474
2588
|
}
|
|
2589
|
+
async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
|
|
2590
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/commits`);
|
|
2591
|
+
url.searchParams.append("per_page", "100");
|
|
2592
|
+
let totalCount = 0;
|
|
2593
|
+
let page = 1;
|
|
2594
|
+
while (true) {
|
|
2595
|
+
url.searchParams.set("page", String(page));
|
|
2596
|
+
const response = await fetch(url.toString(), {
|
|
2597
|
+
...getFetchConfig(),
|
|
2598
|
+
});
|
|
2599
|
+
await handleGitLabError(response);
|
|
2600
|
+
const data = await response.json();
|
|
2601
|
+
if (!Array.isArray(data)) {
|
|
2602
|
+
throw new Error("Unexpected merge request commits response format");
|
|
2603
|
+
}
|
|
2604
|
+
totalCount += data.length;
|
|
2605
|
+
const nextPage = response.headers.get("x-next-page");
|
|
2606
|
+
if (!nextPage) {
|
|
2607
|
+
break;
|
|
2608
|
+
}
|
|
2609
|
+
page = Number.parseInt(nextPage, 10);
|
|
2610
|
+
if (Number.isNaN(page) || page <= 0) {
|
|
2611
|
+
break;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
return totalCount;
|
|
2615
|
+
}
|
|
2616
|
+
async function getProjectMergeMethod(projectId) {
|
|
2617
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`);
|
|
2618
|
+
const response = await fetch(url.toString(), {
|
|
2619
|
+
...getFetchConfig(),
|
|
2620
|
+
});
|
|
2621
|
+
await handleGitLabError(response);
|
|
2622
|
+
const data = await response.json();
|
|
2623
|
+
const mergeMethod = z
|
|
2624
|
+
.object({
|
|
2625
|
+
merge_method: z.string().nullable().optional(),
|
|
2626
|
+
})
|
|
2627
|
+
.parse(data).merge_method;
|
|
2628
|
+
return typeof mergeMethod === "string" ? mergeMethod : null;
|
|
2629
|
+
}
|
|
2630
|
+
function estimateMergeCommitCount(mergeMethod, sourceCommitCount) {
|
|
2631
|
+
if (sourceCommitCount === 0) {
|
|
2632
|
+
return 0;
|
|
2633
|
+
}
|
|
2634
|
+
if (mergeMethod === "merge") {
|
|
2635
|
+
return 1;
|
|
2636
|
+
}
|
|
2637
|
+
if (mergeMethod === "ff" || mergeMethod === "rebase_merge") {
|
|
2638
|
+
return 0;
|
|
2639
|
+
}
|
|
2640
|
+
return null;
|
|
2641
|
+
}
|
|
2642
|
+
async function buildMergeRequestCommitAdditionSummary(projectId, mergeRequest) {
|
|
2643
|
+
try {
|
|
2644
|
+
const sourceCommitCount = await getMergeRequestSourceCommitCount(projectId, mergeRequest.iid);
|
|
2645
|
+
const mergeMethod = await getProjectMergeMethod(projectId);
|
|
2646
|
+
const mergeCommitCount = estimateMergeCommitCount(mergeMethod, sourceCommitCount);
|
|
2647
|
+
const summary = mergeCommitCount === null
|
|
2648
|
+
? null
|
|
2649
|
+
: `${sourceCommitCount} commits and ${mergeCommitCount} merge commit${mergeCommitCount === 1 ? "" : "s"} will be added to ${mergeRequest.target_branch}.`;
|
|
2650
|
+
return {
|
|
2651
|
+
target_branch: mergeRequest.target_branch,
|
|
2652
|
+
source_commits_count: sourceCommitCount,
|
|
2653
|
+
merge_method: mergeMethod,
|
|
2654
|
+
merge_commit_count: mergeCommitCount,
|
|
2655
|
+
summary,
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
catch (error) {
|
|
2659
|
+
const unavailableReason = error instanceof Error ? error.message : String(error);
|
|
2660
|
+
return {
|
|
2661
|
+
target_branch: mergeRequest.target_branch,
|
|
2662
|
+
source_commits_count: null,
|
|
2663
|
+
merge_method: null,
|
|
2664
|
+
merge_commit_count: null,
|
|
2665
|
+
summary: null,
|
|
2666
|
+
unavailable_reason: unavailableReason,
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
async function buildMergeRequestApprovalSummary(projectId, mergeRequestIid) {
|
|
2671
|
+
try {
|
|
2672
|
+
const approvalState = await getMergeRequestApprovalState(projectId, mergeRequestIid);
|
|
2673
|
+
const approvedByUsers = approvalState.approved_by || [];
|
|
2674
|
+
const approvedByUsernames = approvalState.approved_by_usernames || approvedByUsers.map(user => user.username);
|
|
2675
|
+
const inferredApproved = inferMergeRequestApproved(approvalState.rules);
|
|
2676
|
+
return {
|
|
2677
|
+
approved: approvalState.approved ?? inferredApproved,
|
|
2678
|
+
user_has_approved: approvalState.user_has_approved ?? null,
|
|
2679
|
+
user_can_approve: approvalState.user_can_approve ?? null,
|
|
2680
|
+
approved_by: approvedByUsers,
|
|
2681
|
+
approved_by_usernames: approvedByUsernames,
|
|
2682
|
+
rules_count: approvalState.rules?.length ?? null,
|
|
2683
|
+
source_endpoint: approvalState.source_endpoint ?? null,
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
catch (error) {
|
|
2687
|
+
const unavailableReason = error instanceof Error ? error.message : String(error);
|
|
2688
|
+
return {
|
|
2689
|
+
approved: null,
|
|
2690
|
+
user_has_approved: null,
|
|
2691
|
+
user_can_approve: null,
|
|
2692
|
+
approved_by: [],
|
|
2693
|
+
approved_by_usernames: [],
|
|
2694
|
+
rules_count: null,
|
|
2695
|
+
source_endpoint: null,
|
|
2696
|
+
unavailable_reason: unavailableReason,
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
function toMergeRequestDeploymentSummaryRecord(deployment) {
|
|
2701
|
+
return {
|
|
2702
|
+
id: deployment.id,
|
|
2703
|
+
status: deployment.status,
|
|
2704
|
+
ref: deployment.ref,
|
|
2705
|
+
sha: deployment.sha,
|
|
2706
|
+
created_at: deployment.created_at,
|
|
2707
|
+
updated_at: deployment.updated_at,
|
|
2708
|
+
finished_at: deployment.finished_at,
|
|
2709
|
+
web_url: deployment.web_url,
|
|
2710
|
+
environment: deployment.environment
|
|
2711
|
+
? {
|
|
2712
|
+
id: deployment.environment.id,
|
|
2713
|
+
name: deployment.environment.name,
|
|
2714
|
+
slug: deployment.environment.slug,
|
|
2715
|
+
external_url: deployment.environment.external_url,
|
|
2716
|
+
state: deployment.environment.state,
|
|
2717
|
+
tier: deployment.environment.tier,
|
|
2718
|
+
}
|
|
2719
|
+
: undefined,
|
|
2720
|
+
deployable: deployment.deployable === null
|
|
2721
|
+
? null
|
|
2722
|
+
: deployment.deployable
|
|
2723
|
+
? {
|
|
2724
|
+
id: deployment.deployable.id,
|
|
2725
|
+
name: deployment.deployable.name,
|
|
2726
|
+
status: deployment.deployable.status,
|
|
2727
|
+
stage: deployment.deployable.stage,
|
|
2728
|
+
web_url: deployment.deployable.web_url,
|
|
2729
|
+
pipeline: deployment.deployable.pipeline
|
|
2730
|
+
? {
|
|
2731
|
+
id: deployment.deployable.pipeline.id,
|
|
2732
|
+
status: deployment.deployable.pipeline.status,
|
|
2733
|
+
ref: deployment.deployable.pipeline.ref,
|
|
2734
|
+
sha: deployment.deployable.pipeline.sha,
|
|
2735
|
+
web_url: deployment.deployable.pipeline.web_url,
|
|
2736
|
+
}
|
|
2737
|
+
: undefined,
|
|
2738
|
+
}
|
|
2739
|
+
: undefined,
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
function sortDeploymentsByCreatedAtDesc(deployments) {
|
|
2743
|
+
return [...deployments].sort((a, b) => {
|
|
2744
|
+
const aTime = Date.parse(a.created_at);
|
|
2745
|
+
const bTime = Date.parse(b.created_at);
|
|
2746
|
+
if (Number.isNaN(aTime) || Number.isNaN(bTime)) {
|
|
2747
|
+
return b.created_at.localeCompare(a.created_at);
|
|
2748
|
+
}
|
|
2749
|
+
return bTime - aTime;
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
async function buildMergeRequestDeploymentSummary(projectId, mergeRequest) {
|
|
2753
|
+
const lookupSha = mergeRequest.merge_commit_sha ?? mergeRequest.diff_refs?.head_sha ?? null;
|
|
2754
|
+
if (!lookupSha) {
|
|
2755
|
+
return {
|
|
2756
|
+
lookup_sha: null,
|
|
2757
|
+
sort: "created_at_desc",
|
|
2758
|
+
limit: MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT,
|
|
2759
|
+
total_count: 0,
|
|
2760
|
+
returned_count: 0,
|
|
2761
|
+
records: [],
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
try {
|
|
2765
|
+
const deployments = await listDeployments(projectId, {
|
|
2766
|
+
sha: lookupSha,
|
|
2767
|
+
order_by: "created_at",
|
|
2768
|
+
sort: "desc",
|
|
2769
|
+
per_page: 100,
|
|
2770
|
+
});
|
|
2771
|
+
const sortedDeployments = sortDeploymentsByCreatedAtDesc(deployments);
|
|
2772
|
+
const records = sortedDeployments
|
|
2773
|
+
.slice(0, MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT)
|
|
2774
|
+
.map(toMergeRequestDeploymentSummaryRecord);
|
|
2775
|
+
return {
|
|
2776
|
+
lookup_sha: lookupSha,
|
|
2777
|
+
sort: "created_at_desc",
|
|
2778
|
+
limit: MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT,
|
|
2779
|
+
total_count: sortedDeployments.length,
|
|
2780
|
+
returned_count: records.length,
|
|
2781
|
+
records,
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
catch (error) {
|
|
2785
|
+
const unavailableReason = error instanceof Error ? error.message : String(error);
|
|
2786
|
+
return {
|
|
2787
|
+
lookup_sha: lookupSha,
|
|
2788
|
+
sort: "created_at_desc",
|
|
2789
|
+
limit: MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT,
|
|
2790
|
+
total_count: 0,
|
|
2791
|
+
returned_count: 0,
|
|
2792
|
+
records: [],
|
|
2793
|
+
unavailable_reason: unavailableReason,
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2475
2797
|
/**
|
|
2476
2798
|
* Get merge request changes/diffs
|
|
2477
2799
|
* MR 변경사항 조회 함수 (Function to retrieve merge request changes)
|
|
@@ -2637,7 +2959,7 @@ async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPass
|
|
|
2637
2959
|
body: JSON.stringify(body),
|
|
2638
2960
|
});
|
|
2639
2961
|
await handleGitLabError(response);
|
|
2640
|
-
return
|
|
2962
|
+
return parseApprovalsResponse(await response.json());
|
|
2641
2963
|
}
|
|
2642
2964
|
/**
|
|
2643
2965
|
* Unapprove a previously approved merge request
|
|
@@ -2655,7 +2977,7 @@ async function unapproveMergeRequest(projectId, mergeRequestIid) {
|
|
|
2655
2977
|
body: JSON.stringify({}),
|
|
2656
2978
|
});
|
|
2657
2979
|
await handleGitLabError(response);
|
|
2658
|
-
return
|
|
2980
|
+
return parseApprovalsResponse(await response.json());
|
|
2659
2981
|
}
|
|
2660
2982
|
/**
|
|
2661
2983
|
* Get the approval state of a merge request
|
|
@@ -2666,13 +2988,70 @@ async function unapproveMergeRequest(projectId, mergeRequestIid) {
|
|
|
2666
2988
|
*/
|
|
2667
2989
|
async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
|
|
2668
2990
|
projectId = decodeURIComponent(projectId);
|
|
2669
|
-
const
|
|
2670
|
-
const
|
|
2991
|
+
const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approval_state`);
|
|
2992
|
+
const approvalStateResponse = await fetch(approvalStateUrl.toString(), {
|
|
2671
2993
|
...getFetchConfig(),
|
|
2672
2994
|
method: "GET",
|
|
2673
2995
|
});
|
|
2674
|
-
|
|
2675
|
-
|
|
2996
|
+
if (approvalStateResponse.status === 404) {
|
|
2997
|
+
return getMergeRequestApprovalsFallback(projectId, mergeRequestIid);
|
|
2998
|
+
}
|
|
2999
|
+
await handleGitLabError(approvalStateResponse);
|
|
3000
|
+
const parsedApprovalState = GitLabMergeRequestApprovalStateSchema.parse(await approvalStateResponse.json());
|
|
3001
|
+
const approvedByUsers = getUniqueApprovalUsers((parsedApprovalState.rules || []).flatMap(rule => rule.approved_by || []));
|
|
3002
|
+
const approvedByUsernames = approvedByUsers.map(user => user.username);
|
|
3003
|
+
return {
|
|
3004
|
+
...parsedApprovalState,
|
|
3005
|
+
approved_by: approvedByUsers,
|
|
3006
|
+
approved_by_usernames: approvedByUsernames,
|
|
3007
|
+
source_endpoint: "approval_state",
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
async function getMergeRequestApprovalsFallback(projectId, mergeRequestIid) {
|
|
3011
|
+
const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approvals`);
|
|
3012
|
+
const approvalsResponse = await fetch(approvalsUrl.toString(), {
|
|
3013
|
+
...getFetchConfig(),
|
|
3014
|
+
method: "GET",
|
|
3015
|
+
});
|
|
3016
|
+
await handleGitLabError(approvalsResponse);
|
|
3017
|
+
return parseApprovalsResponse(await approvalsResponse.json());
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Parse the response from POST /approve and POST /unapprove endpoints.
|
|
3021
|
+
* These endpoints return the approvals format (approved_by contains nested
|
|
3022
|
+
* { user: {...} } objects), which must be converted to the flat format
|
|
3023
|
+
* used by GitLabMergeRequestApprovalStateSchema.
|
|
3024
|
+
*/
|
|
3025
|
+
function parseApprovalsResponse(responseJson) {
|
|
3026
|
+
const parsedApprovals = GitLabMergeRequestApprovalsResponseSchema.parse(responseJson);
|
|
3027
|
+
const approvedByUsers = getUniqueApprovalUsers((parsedApprovals.approved_by || []).map(approvedByEntry => approvedByEntry.user));
|
|
3028
|
+
const approvedByUsernames = approvedByUsers.map(user => user.username);
|
|
3029
|
+
return GitLabMergeRequestApprovalStateSchema.parse({
|
|
3030
|
+
approved: parsedApprovals.approved,
|
|
3031
|
+
user_has_approved: parsedApprovals.user_has_approved,
|
|
3032
|
+
user_can_approve: parsedApprovals.user_can_approve,
|
|
3033
|
+
approved_by: approvedByUsers,
|
|
3034
|
+
approved_by_usernames: approvedByUsernames,
|
|
3035
|
+
source_endpoint: "approvals",
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
function getUniqueApprovalUsers(users) {
|
|
3039
|
+
const uniqueUsers = new Map();
|
|
3040
|
+
for (const user of users) {
|
|
3041
|
+
if (!uniqueUsers.has(user.id)) {
|
|
3042
|
+
uniqueUsers.set(user.id, user);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
return [...uniqueUsers.values()];
|
|
3046
|
+
}
|
|
3047
|
+
function inferMergeRequestApproved(rules) {
|
|
3048
|
+
if (!rules || rules.length === 0) {
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
if (rules.some(rule => typeof rule.approved !== "boolean")) {
|
|
3052
|
+
return null;
|
|
3053
|
+
}
|
|
3054
|
+
return rules.every(rule => rule.approved === true);
|
|
2676
3055
|
}
|
|
2677
3056
|
/**
|
|
2678
3057
|
* Create a new note (comment) on an issue or merge request
|
|
@@ -3287,6 +3666,89 @@ async function listGroupProjects(options) {
|
|
|
3287
3666
|
const projects = await response.json();
|
|
3288
3667
|
return GitLabProjectSchema.array().parse(projects);
|
|
3289
3668
|
}
|
|
3669
|
+
// Webhook API helper functions
|
|
3670
|
+
/**
|
|
3671
|
+
* Build the base URL for webhooks (project or group)
|
|
3672
|
+
*/
|
|
3673
|
+
function buildWebhookBaseUrl(projectId, groupId) {
|
|
3674
|
+
if (projectId) {
|
|
3675
|
+
projectId = decodeURIComponent(projectId);
|
|
3676
|
+
return `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/hooks`;
|
|
3677
|
+
}
|
|
3678
|
+
const decodedGroupId = decodeURIComponent(groupId);
|
|
3679
|
+
return `${getEffectiveApiUrl()}/groups/${encodeURIComponent(decodedGroupId)}/hooks`;
|
|
3680
|
+
}
|
|
3681
|
+
/**
|
|
3682
|
+
* List webhooks for a project or group
|
|
3683
|
+
*/
|
|
3684
|
+
async function listWebhooks(options) {
|
|
3685
|
+
const url = new URL(buildWebhookBaseUrl(options.project_id, options.group_id));
|
|
3686
|
+
if (options.page)
|
|
3687
|
+
url.searchParams.append("page", options.page.toString());
|
|
3688
|
+
if (options.per_page)
|
|
3689
|
+
url.searchParams.append("per_page", options.per_page.toString());
|
|
3690
|
+
const response = await fetch(url.toString(), { ...getFetchConfig() });
|
|
3691
|
+
await handleGitLabError(response);
|
|
3692
|
+
return (await response.json());
|
|
3693
|
+
}
|
|
3694
|
+
/**
|
|
3695
|
+
* Summarize webhook events by stripping heavy payload fields
|
|
3696
|
+
*/
|
|
3697
|
+
function summarizeWebhookEvents(events) {
|
|
3698
|
+
return events.map(event => ({
|
|
3699
|
+
id: event.id,
|
|
3700
|
+
url: event.url,
|
|
3701
|
+
trigger: event.trigger,
|
|
3702
|
+
response_status: event.response_status,
|
|
3703
|
+
execution_duration: event.execution_duration,
|
|
3704
|
+
}));
|
|
3705
|
+
}
|
|
3706
|
+
/**
|
|
3707
|
+
* Fetch a single page of webhook events
|
|
3708
|
+
*/
|
|
3709
|
+
async function fetchWebhookEventsPage(baseUrl, page, perPage, status) {
|
|
3710
|
+
const url = new URL(baseUrl);
|
|
3711
|
+
url.searchParams.set("page", page.toString());
|
|
3712
|
+
url.searchParams.set("per_page", perPage.toString());
|
|
3713
|
+
if (status !== undefined)
|
|
3714
|
+
url.searchParams.append("status", String(status));
|
|
3715
|
+
const response = await fetch(url.toString(), { ...getFetchConfig() });
|
|
3716
|
+
await handleGitLabError(response);
|
|
3717
|
+
return (await response.json());
|
|
3718
|
+
}
|
|
3719
|
+
/**
|
|
3720
|
+
* List webhook events for a project or group webhook
|
|
3721
|
+
*/
|
|
3722
|
+
async function listWebhookEvents(options) {
|
|
3723
|
+
const eventsUrl = `${buildWebhookBaseUrl(options.project_id, options.group_id)}/${options.hook_id}/events`;
|
|
3724
|
+
const events = await fetchWebhookEventsPage(eventsUrl, options.page ?? 1, options.per_page ?? 20, options.status);
|
|
3725
|
+
return options.summary ? summarizeWebhookEvents(events) : events;
|
|
3726
|
+
}
|
|
3727
|
+
/**
|
|
3728
|
+
* Get a specific webhook event by ID (searches up to 500 recent events)
|
|
3729
|
+
*/
|
|
3730
|
+
async function getWebhookEvent(options) {
|
|
3731
|
+
const eventsUrl = `${buildWebhookBaseUrl(options.project_id, options.group_id)}/${options.hook_id}/events`;
|
|
3732
|
+
// GitLab enforces max per_page=20 for webhook events
|
|
3733
|
+
const perPage = 20;
|
|
3734
|
+
if (options.page) {
|
|
3735
|
+
// Direct page lookup — single API call
|
|
3736
|
+
const events = await fetchWebhookEventsPage(eventsUrl, options.page, perPage);
|
|
3737
|
+
const match = events.find(e => e.id === options.event_id);
|
|
3738
|
+
return match ?? null;
|
|
3739
|
+
}
|
|
3740
|
+
// Auto-paginate up to 500 events
|
|
3741
|
+
const maxPages = 25;
|
|
3742
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
3743
|
+
const events = await fetchWebhookEventsPage(eventsUrl, page, perPage);
|
|
3744
|
+
const match = events.find(e => e.id === options.event_id);
|
|
3745
|
+
if (match)
|
|
3746
|
+
return match;
|
|
3747
|
+
if (events.length < perPage)
|
|
3748
|
+
break;
|
|
3749
|
+
}
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3290
3752
|
// Wiki API helper functions
|
|
3291
3753
|
/**
|
|
3292
3754
|
* List wiki pages in a project
|
|
@@ -3409,6 +3871,90 @@ async function getPipeline(projectId, pipelineId) {
|
|
|
3409
3871
|
const data = await response.json();
|
|
3410
3872
|
return GitLabPipelineSchema.parse(data);
|
|
3411
3873
|
}
|
|
3874
|
+
/**
|
|
3875
|
+
* List deployments in a GitLab project
|
|
3876
|
+
*
|
|
3877
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3878
|
+
* @param {ListDeploymentsOptions} options - Options for filtering deployments
|
|
3879
|
+
* @returns {Promise<GitLabDeployment[]>} List of deployments
|
|
3880
|
+
*/
|
|
3881
|
+
async function listDeployments(projectId, options = {}) {
|
|
3882
|
+
projectId = decodeURIComponent(projectId);
|
|
3883
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments`);
|
|
3884
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
3885
|
+
if (value !== undefined) {
|
|
3886
|
+
url.searchParams.append(key, value.toString());
|
|
3887
|
+
}
|
|
3888
|
+
});
|
|
3889
|
+
const response = await fetch(url.toString(), {
|
|
3890
|
+
...getFetchConfig(),
|
|
3891
|
+
});
|
|
3892
|
+
await handleGitLabError(response);
|
|
3893
|
+
const data = await response.json();
|
|
3894
|
+
return z.array(GitLabDeploymentSchema).parse(data);
|
|
3895
|
+
}
|
|
3896
|
+
/**
|
|
3897
|
+
* Get details of a specific deployment
|
|
3898
|
+
*
|
|
3899
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3900
|
+
* @param {number | string} deploymentId - The ID of the deployment
|
|
3901
|
+
* @returns {Promise<GitLabDeployment>} Deployment details
|
|
3902
|
+
*/
|
|
3903
|
+
async function getDeployment(projectId, deploymentId) {
|
|
3904
|
+
projectId = decodeURIComponent(projectId);
|
|
3905
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments/${deploymentId}`);
|
|
3906
|
+
const response = await fetch(url.toString(), {
|
|
3907
|
+
...getFetchConfig(),
|
|
3908
|
+
});
|
|
3909
|
+
if (response.status === 404) {
|
|
3910
|
+
throw new Error(`Deployment not found`);
|
|
3911
|
+
}
|
|
3912
|
+
await handleGitLabError(response);
|
|
3913
|
+
const data = await response.json();
|
|
3914
|
+
return GitLabDeploymentSchema.parse(data);
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* List environments in a GitLab project
|
|
3918
|
+
*
|
|
3919
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3920
|
+
* @param {ListEnvironmentsOptions} options - Options for filtering environments
|
|
3921
|
+
* @returns {Promise<GitLabEnvironment[]>} List of environments
|
|
3922
|
+
*/
|
|
3923
|
+
async function listEnvironments(projectId, options = {}) {
|
|
3924
|
+
projectId = decodeURIComponent(projectId);
|
|
3925
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments`);
|
|
3926
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
3927
|
+
if (value !== undefined) {
|
|
3928
|
+
url.searchParams.append(key, value.toString());
|
|
3929
|
+
}
|
|
3930
|
+
});
|
|
3931
|
+
const response = await fetch(url.toString(), {
|
|
3932
|
+
...getFetchConfig(),
|
|
3933
|
+
});
|
|
3934
|
+
await handleGitLabError(response);
|
|
3935
|
+
const data = await response.json();
|
|
3936
|
+
return z.array(GitLabEnvironmentSchema).parse(data);
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Get details of a specific environment
|
|
3940
|
+
*
|
|
3941
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3942
|
+
* @param {number | string} environmentId - The ID of the environment
|
|
3943
|
+
* @returns {Promise<GitLabEnvironment>} Environment details
|
|
3944
|
+
*/
|
|
3945
|
+
async function getEnvironment(projectId, environmentId) {
|
|
3946
|
+
projectId = decodeURIComponent(projectId);
|
|
3947
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments/${environmentId}`);
|
|
3948
|
+
const response = await fetch(url.toString(), {
|
|
3949
|
+
...getFetchConfig(),
|
|
3950
|
+
});
|
|
3951
|
+
if (response.status === 404) {
|
|
3952
|
+
throw new Error(`Environment not found`);
|
|
3953
|
+
}
|
|
3954
|
+
await handleGitLabError(response);
|
|
3955
|
+
const data = await response.json();
|
|
3956
|
+
return GitLabEnvironmentSchema.parse(data);
|
|
3957
|
+
}
|
|
3412
3958
|
/**
|
|
3413
3959
|
* List all jobs in a specific pipeline
|
|
3414
3960
|
*
|
|
@@ -3533,21 +4079,107 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
|
|
|
3533
4079
|
}
|
|
3534
4080
|
return fullTrace;
|
|
3535
4081
|
}
|
|
4082
|
+
/**
|
|
4083
|
+
* List artifact files in a job's artifacts archive
|
|
4084
|
+
*
|
|
4085
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
4086
|
+
* @param {string} jobId - The ID of the job
|
|
4087
|
+
* @param {Object} options - Options for listing artifacts
|
|
4088
|
+
* @returns {Promise<GitLabArtifactEntry[]>} List of artifact entries
|
|
4089
|
+
*/
|
|
4090
|
+
async function listJobArtifacts(projectId, jobId, options = {}) {
|
|
4091
|
+
projectId = decodeURIComponent(projectId);
|
|
4092
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/artifacts/tree`);
|
|
4093
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
4094
|
+
if (value !== undefined) {
|
|
4095
|
+
if (typeof value === "boolean") {
|
|
4096
|
+
url.searchParams.append(key, value ? "true" : "false");
|
|
4097
|
+
}
|
|
4098
|
+
else {
|
|
4099
|
+
url.searchParams.append(key, value.toString());
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
});
|
|
4103
|
+
const response = await fetch(url.toString(), {
|
|
4104
|
+
...getFetchConfig(),
|
|
4105
|
+
});
|
|
4106
|
+
if (response.status === 404) {
|
|
4107
|
+
throw new Error(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
|
|
4108
|
+
}
|
|
4109
|
+
await handleGitLabError(response);
|
|
4110
|
+
const data = await response.json();
|
|
4111
|
+
return z.array(GitLabArtifactEntrySchema).parse(data);
|
|
4112
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* Download the entire artifact archive for a job and save to disk
|
|
4115
|
+
*
|
|
4116
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
4117
|
+
* @param {string} jobId - The ID of the job
|
|
4118
|
+
* @param {string} localPath - Optional local directory to save the archive
|
|
4119
|
+
* @returns {Promise<string>} The path where the artifact archive was saved
|
|
4120
|
+
*/
|
|
4121
|
+
async function downloadJobArtifacts(projectId, jobId, localPath) {
|
|
4122
|
+
projectId = decodeURIComponent(projectId);
|
|
4123
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
4124
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${jobId}/artifacts`);
|
|
4125
|
+
const response = await fetch(url.toString(), {
|
|
4126
|
+
...getFetchConfig(),
|
|
4127
|
+
});
|
|
4128
|
+
if (response.status === 404) {
|
|
4129
|
+
throw new Error(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
|
|
4130
|
+
}
|
|
4131
|
+
await handleGitLabError(response);
|
|
4132
|
+
const buffer = await response.arrayBuffer();
|
|
4133
|
+
const filename = `artifacts_job_${jobId}.zip`;
|
|
4134
|
+
const savePath = localPath ? path.join(localPath, filename) : filename;
|
|
4135
|
+
fs.mkdirSync(path.dirname(savePath), { recursive: true });
|
|
4136
|
+
fs.writeFileSync(savePath, Buffer.from(buffer));
|
|
4137
|
+
return savePath;
|
|
4138
|
+
}
|
|
4139
|
+
/**
|
|
4140
|
+
* Download a single file from a job's artifacts
|
|
4141
|
+
*
|
|
4142
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
4143
|
+
* @param {string} jobId - The ID of the job
|
|
4144
|
+
* @param {string} artifactPath - Path to the file within the artifacts archive
|
|
4145
|
+
* @returns {Promise<string>} The file content as text
|
|
4146
|
+
*/
|
|
4147
|
+
async function getJobArtifactFile(projectId, jobId, artifactPath) {
|
|
4148
|
+
projectId = decodeURIComponent(projectId);
|
|
4149
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
4150
|
+
const encodedArtifactPath = artifactPath
|
|
4151
|
+
.split("/")
|
|
4152
|
+
.map(segment => encodeURIComponent(segment))
|
|
4153
|
+
.join("/");
|
|
4154
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${jobId}/artifacts/${encodedArtifactPath}`);
|
|
4155
|
+
const response = await fetch(url.toString(), {
|
|
4156
|
+
...getFetchConfig(),
|
|
4157
|
+
});
|
|
4158
|
+
if (response.status === 404) {
|
|
4159
|
+
throw new Error(`Artifact file not found: ${artifactPath}`);
|
|
4160
|
+
}
|
|
4161
|
+
await handleGitLabError(response);
|
|
4162
|
+
return await response.text();
|
|
4163
|
+
}
|
|
3536
4164
|
/**
|
|
3537
4165
|
* Create a new pipeline
|
|
3538
4166
|
*
|
|
3539
4167
|
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3540
4168
|
* @param {string} ref - The branch or tag to run the pipeline on
|
|
3541
4169
|
* @param {Array} variables - Optional variables for the pipeline
|
|
4170
|
+
* @param {Record<string, string>} inputs - Optional input parameters for the pipeline
|
|
3542
4171
|
* @returns {Promise<GitLabPipeline>} The created pipeline
|
|
3543
4172
|
*/
|
|
3544
|
-
async function createPipeline(projectId, ref, variables) {
|
|
4173
|
+
async function createPipeline(projectId, ref, variables, inputs) {
|
|
3545
4174
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
3546
4175
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline`);
|
|
3547
4176
|
const body = { ref };
|
|
3548
4177
|
if (variables && variables.length > 0) {
|
|
3549
4178
|
body.variables = variables;
|
|
3550
4179
|
}
|
|
4180
|
+
if (inputs && Object.keys(inputs).length > 0) {
|
|
4181
|
+
body.inputs = inputs;
|
|
4182
|
+
}
|
|
3551
4183
|
const response = await fetch(url.toString(), {
|
|
3552
4184
|
method: "POST",
|
|
3553
4185
|
headers: {
|
|
@@ -4450,6 +5082,8 @@ async function handleToolCall(params) {
|
|
|
4450
5082
|
if (GITLAB_AUTH_COOKIE_PATH) {
|
|
4451
5083
|
await ensureSessionForRequest();
|
|
4452
5084
|
}
|
|
5085
|
+
// Lazy OAuth token refresh: only validate/refresh when a tool is actually called
|
|
5086
|
+
await ensureValidOAuthToken();
|
|
4453
5087
|
logger.info(params.name);
|
|
4454
5088
|
switch (params.name) {
|
|
4455
5089
|
case "execute_graphql": {
|
|
@@ -4678,8 +5312,22 @@ async function handleToolCall(params) {
|
|
|
4678
5312
|
case "get_merge_request": {
|
|
4679
5313
|
const args = GetMergeRequestSchema.parse(params.arguments);
|
|
4680
5314
|
const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid, args.source_branch);
|
|
5315
|
+
const deploymentSummary = await buildMergeRequestDeploymentSummary(args.project_id, mergeRequest);
|
|
5316
|
+
const commitAdditionSummary = await buildMergeRequestCommitAdditionSummary(args.project_id, mergeRequest);
|
|
5317
|
+
const approvalSummary = await buildMergeRequestApprovalSummary(args.project_id, mergeRequest.iid);
|
|
5318
|
+
const mergeRequestWithDeploymentSummary = {
|
|
5319
|
+
...mergeRequest,
|
|
5320
|
+
deployment_summary: deploymentSummary,
|
|
5321
|
+
commit_addition_summary: commitAdditionSummary,
|
|
5322
|
+
approval_summary: approvalSummary,
|
|
5323
|
+
};
|
|
4681
5324
|
return {
|
|
4682
|
-
content: [
|
|
5325
|
+
content: [
|
|
5326
|
+
{
|
|
5327
|
+
type: "text",
|
|
5328
|
+
text: JSON.stringify(mergeRequestWithDeploymentSummary, null, 2),
|
|
5329
|
+
},
|
|
5330
|
+
],
|
|
4683
5331
|
};
|
|
4684
5332
|
}
|
|
4685
5333
|
case "get_merge_request_diffs": {
|
|
@@ -5138,6 +5786,36 @@ async function handleToolCall(params) {
|
|
|
5138
5786
|
],
|
|
5139
5787
|
};
|
|
5140
5788
|
}
|
|
5789
|
+
case "list_deployments": {
|
|
5790
|
+
const args = ListDeploymentsSchema.parse(params.arguments);
|
|
5791
|
+
const { project_id, ...options } = args;
|
|
5792
|
+
const deployments = await listDeployments(project_id, options);
|
|
5793
|
+
return {
|
|
5794
|
+
content: [{ type: "text", text: JSON.stringify(deployments, null, 2) }],
|
|
5795
|
+
};
|
|
5796
|
+
}
|
|
5797
|
+
case "get_deployment": {
|
|
5798
|
+
const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
|
|
5799
|
+
const deployment = await getDeployment(project_id, deployment_id);
|
|
5800
|
+
return {
|
|
5801
|
+
content: [{ type: "text", text: JSON.stringify(deployment, null, 2) }],
|
|
5802
|
+
};
|
|
5803
|
+
}
|
|
5804
|
+
case "list_environments": {
|
|
5805
|
+
const args = ListEnvironmentsSchema.parse(params.arguments);
|
|
5806
|
+
const { project_id, ...options } = args;
|
|
5807
|
+
const environments = await listEnvironments(project_id, options);
|
|
5808
|
+
return {
|
|
5809
|
+
content: [{ type: "text", text: JSON.stringify(environments, null, 2) }],
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
case "get_environment": {
|
|
5813
|
+
const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
|
|
5814
|
+
const environment = await getEnvironment(project_id, environment_id);
|
|
5815
|
+
return {
|
|
5816
|
+
content: [{ type: "text", text: JSON.stringify(environment, null, 2) }],
|
|
5817
|
+
};
|
|
5818
|
+
}
|
|
5141
5819
|
case "list_pipeline_jobs": {
|
|
5142
5820
|
const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(params.arguments);
|
|
5143
5821
|
const jobs = await listPipelineJobs(project_id, pipeline_id, options);
|
|
@@ -5187,8 +5865,8 @@ async function handleToolCall(params) {
|
|
|
5187
5865
|
};
|
|
5188
5866
|
}
|
|
5189
5867
|
case "create_pipeline": {
|
|
5190
|
-
const { project_id, ref, variables } = CreatePipelineSchema.parse(params.arguments);
|
|
5191
|
-
const pipeline = await createPipeline(project_id, ref, variables);
|
|
5868
|
+
const { project_id, ref, variables, inputs } = CreatePipelineSchema.parse(params.arguments);
|
|
5869
|
+
const pipeline = await createPipeline(project_id, ref, variables, inputs);
|
|
5192
5870
|
return {
|
|
5193
5871
|
content: [
|
|
5194
5872
|
{
|
|
@@ -5258,6 +5936,42 @@ async function handleToolCall(params) {
|
|
|
5258
5936
|
],
|
|
5259
5937
|
};
|
|
5260
5938
|
}
|
|
5939
|
+
case "list_job_artifacts": {
|
|
5940
|
+
const { project_id, job_id, ...options } = ListJobArtifactsSchema.parse(params.arguments);
|
|
5941
|
+
const artifacts = await listJobArtifacts(project_id, job_id, options);
|
|
5942
|
+
return {
|
|
5943
|
+
content: [
|
|
5944
|
+
{
|
|
5945
|
+
type: "text",
|
|
5946
|
+
text: JSON.stringify(artifacts, null, 2),
|
|
5947
|
+
},
|
|
5948
|
+
],
|
|
5949
|
+
};
|
|
5950
|
+
}
|
|
5951
|
+
case "download_job_artifacts": {
|
|
5952
|
+
const { project_id, job_id, local_path } = DownloadJobArtifactsSchema.parse(params.arguments);
|
|
5953
|
+
const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
|
|
5954
|
+
return {
|
|
5955
|
+
content: [
|
|
5956
|
+
{
|
|
5957
|
+
type: "text",
|
|
5958
|
+
text: JSON.stringify({ success: true, file_path: filePath }, null, 2),
|
|
5959
|
+
},
|
|
5960
|
+
],
|
|
5961
|
+
};
|
|
5962
|
+
}
|
|
5963
|
+
case "get_job_artifact_file": {
|
|
5964
|
+
const { project_id, job_id, artifact_path } = GetJobArtifactFileSchema.parse(params.arguments);
|
|
5965
|
+
const fileContent = await getJobArtifactFile(project_id, job_id, artifact_path);
|
|
5966
|
+
return {
|
|
5967
|
+
content: [
|
|
5968
|
+
{
|
|
5969
|
+
type: "text",
|
|
5970
|
+
text: fileContent,
|
|
5971
|
+
},
|
|
5972
|
+
],
|
|
5973
|
+
};
|
|
5974
|
+
}
|
|
5261
5975
|
case "list_merge_requests": {
|
|
5262
5976
|
const args = ListMergeRequestsSchema.parse(params.arguments);
|
|
5263
5977
|
const mergeRequests = await listMergeRequests(args.project_id, args);
|
|
@@ -5513,6 +6227,40 @@ async function handleToolCall(params) {
|
|
|
5513
6227
|
content: [{ type: "text", text: assetContent }],
|
|
5514
6228
|
};
|
|
5515
6229
|
}
|
|
6230
|
+
case "list_webhooks": {
|
|
6231
|
+
const args = ListWebhooksSchema.parse(params.arguments);
|
|
6232
|
+
const webhooks = await listWebhooks(args);
|
|
6233
|
+
return {
|
|
6234
|
+
content: [{ type: "text", text: JSON.stringify(webhooks, null, 2) }],
|
|
6235
|
+
};
|
|
6236
|
+
}
|
|
6237
|
+
case "list_webhook_events": {
|
|
6238
|
+
const args = ListWebhookEventsSchema.parse(params.arguments);
|
|
6239
|
+
const events = await listWebhookEvents(args);
|
|
6240
|
+
return {
|
|
6241
|
+
content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
|
|
6242
|
+
};
|
|
6243
|
+
}
|
|
6244
|
+
case "get_webhook_event": {
|
|
6245
|
+
const args = GetWebhookEventSchema.parse(params.arguments);
|
|
6246
|
+
const event = await getWebhookEvent(args);
|
|
6247
|
+
if (!event) {
|
|
6248
|
+
const searchScope = args.page
|
|
6249
|
+
? `on page ${args.page}`
|
|
6250
|
+
: "in the 500 most recent events";
|
|
6251
|
+
return {
|
|
6252
|
+
content: [
|
|
6253
|
+
{
|
|
6254
|
+
type: "text",
|
|
6255
|
+
text: JSON.stringify({ error: `Webhook event ${args.event_id} not found ${searchScope}` }, null, 2),
|
|
6256
|
+
},
|
|
6257
|
+
],
|
|
6258
|
+
};
|
|
6259
|
+
}
|
|
6260
|
+
return {
|
|
6261
|
+
content: [{ type: "text", text: JSON.stringify(event, null, 2) }],
|
|
6262
|
+
};
|
|
6263
|
+
}
|
|
5516
6264
|
default:
|
|
5517
6265
|
throw new Error(`Unknown tool: ${params.name}`);
|
|
5518
6266
|
}
|
|
@@ -6004,9 +6752,10 @@ async function runServer() {
|
|
|
6004
6752
|
logger.info("Using OAuth authentication...");
|
|
6005
6753
|
try {
|
|
6006
6754
|
const gitlabBaseUrl = GITLAB_API_URL.replace(/\/api\/v4$/, "");
|
|
6007
|
-
|
|
6755
|
+
const oauthResult = await initializeOAuthClient(gitlabBaseUrl);
|
|
6756
|
+
oauthClient = oauthResult.client;
|
|
6757
|
+
OAUTH_ACCESS_TOKEN = oauthResult.accessToken;
|
|
6008
6758
|
logger.info("OAuth authentication successful");
|
|
6009
|
-
// Note: Headers are automatically generated by buildAuthHeaders() using OAUTH_ACCESS_TOKEN
|
|
6010
6759
|
}
|
|
6011
6760
|
catch (error) {
|
|
6012
6761
|
logger.error("OAuth authentication failed:", error);
|