@zereight/mcp-gitlab 2.0.32 → 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/build/index.js CHANGED
@@ -50,7 +50,7 @@ GitLabDiscussionNoteSchema, // Added
50
50
  GitLabDiscussionSchema,
51
51
  // Draft Notes Schemas
52
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";
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,6 +243,7 @@ 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;
246
248
  let oauthClient = null;
247
249
  /**
@@ -492,6 +494,10 @@ function buildAuthHeaders() {
492
494
  }
493
495
  return {}; // No auth headers if no session context
494
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
+ }
495
501
  // Standard mode: prioritize OAuth token, then fall back to environment token
496
502
  const token = OAUTH_ACCESS_TOKEN || GITLAB_PERSONAL_ACCESS_TOKEN;
497
503
  if (IS_OLD && token) {
@@ -1152,6 +1158,21 @@ const allTools = [
1152
1158
  description: "Download a release asset file by direct asset path",
1153
1159
  inputSchema: toJSONSchema(DownloadReleaseAssetSchema),
1154
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
+ },
1155
1176
  ];
1156
1177
  // Define which tools are read-only
1157
1178
  const readOnlyTools = new Set([
@@ -1214,6 +1235,9 @@ const readOnlyTools = new Set([
1214
1235
  "get_release",
1215
1236
  "download_release_asset",
1216
1237
  "get_merge_request_approval_state",
1238
+ "list_webhooks",
1239
+ "list_webhook_events",
1240
+ "get_webhook_event",
1217
1241
  ]);
1218
1242
  // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
1219
1243
  const wikiToolNames = new Set([
@@ -1439,6 +1463,15 @@ const TOOLSET_DEFINITIONS = [
1439
1463
  "download_attachment",
1440
1464
  ]),
1441
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
+ },
1442
1475
  ];
1443
1476
  // Derived lookup: tool name → toolset ID
1444
1477
  const TOOLSET_BY_TOOL_NAME = new Map();
@@ -2926,7 +2959,7 @@ async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPass
2926
2959
  body: JSON.stringify(body),
2927
2960
  });
2928
2961
  await handleGitLabError(response);
2929
- return GitLabMergeRequestApprovalStateSchema.parse(await response.json());
2962
+ return parseApprovalsResponse(await response.json());
2930
2963
  }
2931
2964
  /**
2932
2965
  * Unapprove a previously approved merge request
@@ -2944,7 +2977,7 @@ async function unapproveMergeRequest(projectId, mergeRequestIid) {
2944
2977
  body: JSON.stringify({}),
2945
2978
  });
2946
2979
  await handleGitLabError(response);
2947
- return GitLabMergeRequestApprovalStateSchema.parse(await response.json());
2980
+ return parseApprovalsResponse(await response.json());
2948
2981
  }
2949
2982
  /**
2950
2983
  * Get the approval state of a merge request
@@ -2981,7 +3014,16 @@ async function getMergeRequestApprovalsFallback(projectId, mergeRequestIid) {
2981
3014
  method: "GET",
2982
3015
  });
2983
3016
  await handleGitLabError(approvalsResponse);
2984
- const parsedApprovals = GitLabMergeRequestApprovalsResponseSchema.parse(await approvalsResponse.json());
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);
2985
3027
  const approvedByUsers = getUniqueApprovalUsers((parsedApprovals.approved_by || []).map(approvedByEntry => approvedByEntry.user));
2986
3028
  const approvedByUsernames = approvedByUsers.map(user => user.username);
2987
3029
  return GitLabMergeRequestApprovalStateSchema.parse({
@@ -3624,6 +3666,89 @@ async function listGroupProjects(options) {
3624
3666
  const projects = await response.json();
3625
3667
  return GitLabProjectSchema.array().parse(projects);
3626
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
+ }
3627
3752
  // Wiki API helper functions
3628
3753
  /**
3629
3754
  * List wiki pages in a project
@@ -6102,6 +6227,40 @@ async function handleToolCall(params) {
6102
6227
  content: [{ type: "text", text: assetContent }],
6103
6228
  };
6104
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
+ }
6105
6264
  default:
6106
6265
  throw new Error(`Unknown tool: ${params.name}`);
6107
6266
  }
package/build/schemas.js CHANGED
@@ -2449,3 +2449,69 @@ export const DownloadReleaseAssetSchema = z.object({
2449
2449
  .string()
2450
2450
  .describe("Path to the release asset file as specified when creating or updating its link"),
2451
2451
  });
2452
+ // --- Webhook schemas ---
2453
+ export const ListWebhooksSchema = z
2454
+ .object({
2455
+ project_id: z.coerce
2456
+ .string()
2457
+ .optional()
2458
+ .describe("Project ID or URL-encoded path. Provide either project_id or group_id, not both."),
2459
+ group_id: z.coerce
2460
+ .string()
2461
+ .optional()
2462
+ .describe("Group ID or URL-encoded path. Provide either project_id or group_id, not both."),
2463
+ })
2464
+ .merge(PaginationOptionsSchema)
2465
+ .refine(data => (data.project_id || data.group_id) && !(data.project_id && data.group_id), {
2466
+ message: "Provide exactly one of project_id or group_id",
2467
+ });
2468
+ export const ListWebhookEventsSchema = z
2469
+ .object({
2470
+ project_id: z.coerce
2471
+ .string()
2472
+ .optional()
2473
+ .describe("Project ID or URL-encoded path. Provide either project_id or group_id, not both."),
2474
+ group_id: z.coerce
2475
+ .string()
2476
+ .optional()
2477
+ .describe("Group ID or URL-encoded path. Provide either project_id or group_id, not both."),
2478
+ hook_id: z.coerce.number().describe("ID of the webhook"),
2479
+ status: z
2480
+ .union([z.number(), z.string()])
2481
+ .optional()
2482
+ .describe("Filter by response status code (e.g. 200, 500) or category: successful, client_failure, server_failure"),
2483
+ summary: z
2484
+ .boolean()
2485
+ .optional()
2486
+ .describe("If true, return only summary fields (id, url, trigger, response_status, execution_duration) without full request/response payloads. Recommended for overview queries to avoid huge responses."),
2487
+ per_page: z
2488
+ .number()
2489
+ .max(20)
2490
+ .optional()
2491
+ .default(20)
2492
+ .describe("Number of events per page"),
2493
+ page: z.number().optional().describe("Page number for pagination"),
2494
+ })
2495
+ .refine(data => (data.project_id || data.group_id) && !(data.project_id && data.group_id), {
2496
+ message: "Provide exactly one of project_id or group_id",
2497
+ });
2498
+ export const GetWebhookEventSchema = z
2499
+ .object({
2500
+ project_id: z.coerce
2501
+ .string()
2502
+ .optional()
2503
+ .describe("Project ID or URL-encoded path. Provide either project_id or group_id, not both."),
2504
+ group_id: z.coerce
2505
+ .string()
2506
+ .optional()
2507
+ .describe("Group ID or URL-encoded path. Provide either project_id or group_id, not both."),
2508
+ hook_id: z.coerce.number().describe("ID of the webhook"),
2509
+ event_id: z.coerce.number().describe("ID of the webhook event to retrieve"),
2510
+ page: z
2511
+ .number()
2512
+ .optional()
2513
+ .describe("If known, the page where the event is located (from list_webhook_events). Skips auto-pagination and fetches only this page."),
2514
+ })
2515
+ .refine(data => (data.project_id || data.group_id) && !(data.project_id && data.group_id), {
2516
+ message: "Provide exactly one of project_id or group_id",
2517
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.0.32",
3
+ "version": "2.0.33",
4
4
  "description": "MCP server for using the GitLab API",
5
5
  "license": "MIT",
6
6
  "author": "zereight",