@zereight/mcp-gitlab 2.1.62 → 2.1.64

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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_PERMISSION_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, } from "./config.js";
2
+ import { getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_MASKING_ENABLED, GITLAB_MASKING_CONFIG, GITLAB_MASKING_POLICY_FILE, GITLAB_MASKING_WORKSPACE_DIR, GITLAB_PERMISSION_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, } from "./config.js";
3
3
  /** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
4
4
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
5
5
  const STREAMABLE_HTTP_AUTH_TOKEN = getConfig("streamable-http-auth-token", "STREAMABLE_HTTP_AUTH_TOKEN");
@@ -95,11 +95,12 @@ import { graphqlQueryContainsWriteOperation, graphqlQueryContainsDeleteOperation
95
95
  import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
96
96
  import { encodeRepoFilePayloadContent, fileOperationsIncludeDeleteOrMove, toGitLabCommitActions, } from "./utils/gitlab-commit-actions.js";
97
97
  import { redactSensitiveGitLabFields } from "./utils/redact-sensitive.js";
98
+ import { createMaskingPolicyResolver, getManagedMaskingProjectIds, } from "./masking/index.js";
98
99
  import { checkForNewVersion } from "./utils/version-check.js";
99
100
  import { assertGitLabVersionAtLeast } from "./utils/gitlab-version-gate.js";
100
101
  import { parseGitLabVersionApiResponse, } from "./utils/gitlab-instance-version.js";
101
102
  import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
102
- import { cleanMutuallyExclusiveIdUsernameOptions, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
103
+ import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
103
104
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
104
105
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
105
106
  import { GitLabClientPool } from "./gitlab-client-pool.js";
@@ -111,10 +112,18 @@ GitLabDiscussionNoteSchema, // Added
111
112
  GitLabDiscussionSchema,
112
113
  // Draft Notes Schemas
113
114
  GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabGroupMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineScheduleSchema, GitLabPipelineScheduleVariableSchema, GitLabPipelineSchedulePlayResultSchema, ListPipelineSchedulesSchema, GetPipelineScheduleSchema, ListPipelineSchedulePipelinesSchema, CreatePipelineScheduleSchema, UpdatePipelineScheduleSchema, DeletePipelineScheduleSchema, PlayPipelineScheduleSchema, TakeOwnershipPipelineScheduleSchema, GetPipelineScheduleVariableSchema, CreatePipelineScheduleVariableSchema, UpdatePipelineScheduleVariableSchema, DeletePipelineScheduleVariableSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, GitLabCiVariableSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GitLabDependencyProxySchema, GitLabDependencyProxyBlobSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
114
- GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, CreateWebhookSchema, UpdateWebhookSchema, DeleteWebhookSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
115
+ GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, GetMergeRequestDiscussionSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, CreateWebhookSchema, UpdateWebhookSchema, DeleteWebhookSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
115
116
  import { randomUUID, timingSafeEqual, } from "node:crypto";
116
117
  import { createLogger } from "./utils/logger.js";
117
118
  const logger = createLogger();
119
+ // Construct once at startup. When disabled this is undefined and the result
120
+ // path below returns the original objects without reading a config file.
121
+ const maskingPolicyResolver = createMaskingPolicyResolver({
122
+ enabled: GITLAB_MASKING_ENABLED,
123
+ configPath: GITLAB_MASKING_CONFIG,
124
+ policyFilePath: GITLAB_MASKING_POLICY_FILE,
125
+ workspaceDir: GITLAB_MASKING_WORKSPACE_DIR,
126
+ });
118
127
  const SERVER_NAME = process.env.MCP_SERVER_NAME?.trim() || "zereight-gitlab-mcp-server";
119
128
  /**
120
129
  * Static MCP prompt catalog. These wrap the multi-step tool workflows documented in
@@ -292,22 +301,74 @@ function createServer() {
292
301
  const sessionId = request.params.sessionId;
293
302
  const toolName = request.params.name;
294
303
  const start = Date.now();
304
+ let maskingEngine;
295
305
  const logCompletion = (result) => {
296
306
  const durationMs = Date.now() - start;
297
307
  logger.info({ tool: toolName, event: "tool_call_done", durationMs }, `tool_call_done: ${toolName} (${durationMs}ms)`);
298
- return result;
308
+ const isPlainTextResult = toolName === "get_pipeline_job_output" ||
309
+ toolName === "get_job_artifact_file" ||
310
+ (toolName === "download_release_asset" && !IS_REMOTE);
311
+ return maskingEngine
312
+ ? maskingEngine.maskToolResult(result, { textFormat: isPlainTextResult ? "plain" : "json" })
313
+ : result;
299
314
  };
300
315
  const logError = (error) => {
301
316
  const durationMs = Date.now() - start;
317
+ const safeError = maskingEngine ? maskingEngine.maskError(error) : error;
302
318
  logger.error({
303
319
  tool: toolName,
304
320
  event: "tool_call_error",
305
321
  durationMs,
306
- error: error instanceof Error ? error.message : String(error),
322
+ error: safeError instanceof Error ? safeError.message : String(safeError),
307
323
  }, `tool_call_error: ${toolName} (${durationMs}ms)`);
308
- throw error;
324
+ throw safeError;
309
325
  };
310
326
  try {
327
+ const authData = (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) && sessionId
328
+ ? authBySession[sessionId]
329
+ : undefined;
330
+ const sessionContext = authData
331
+ ? {
332
+ sessionId,
333
+ header: authData.header,
334
+ token: authData.token,
335
+ lastUsed: authData.lastUsed,
336
+ apiUrl: authData.apiUrl,
337
+ publicBaseUrl: authData.publicBaseUrl,
338
+ allowedProjectIds: authData.allowedProjectIds,
339
+ }
340
+ : undefined;
341
+ const selectMaskingPolicy = () => {
342
+ const isToolDiscovery = toolName === "discover_tools";
343
+ const inputSchema = allTools.find(tool => tool.name === toolName)?.inputSchema;
344
+ // Optional project fields can be filters on global queries. Only supply
345
+ // the default when the corresponding handler actually uses it. Keep this
346
+ // list synchronized with getFileContents, listMyIssues, listIssues, and
347
+ // listMergeRequests: those are the handlers with optional project input
348
+ // that may call getEffectiveProjectId("") at runtime.
349
+ const usesDefaultProject = inputSchema?.required?.includes("project_id") ||
350
+ toolName === "get_file_contents" ||
351
+ (toolName === "my_issues" &&
352
+ (Boolean(GITLAB_PROJECT_ID) || getEffectiveAllowedProjectIds().length > 0)) ||
353
+ ((toolName === "list_issues" || toolName === "list_merge_requests") &&
354
+ ENABLE_STRICT_PROJECT_SCOPE &&
355
+ getEffectiveAllowedProjectIds().length > 0);
356
+ const maskingScope = maskingPolicyResolver
357
+ ? getManagedMaskingProjectIds(inputSchema, request.params.arguments, maskingPolicyResolver.managed && usesDefaultProject
358
+ ? () => getEffectiveProjectId("")
359
+ : undefined)
360
+ : undefined;
361
+ return maskingPolicyResolver?.select({
362
+ gitlabInstance: getEffectiveApiUrl(),
363
+ ...maskingScope,
364
+ // Tool discovery does not access GitLab data and must remain available
365
+ // under the default managed-policy setting.
366
+ allowBuiltinForUnscoped: isToolDiscovery,
367
+ });
368
+ };
369
+ maskingEngine = sessionContext
370
+ ? sessionAuthStore.run(sessionContext, selectMaskingPolicy)
371
+ : selectMaskingPolicy();
311
372
  // Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
312
373
  if (toolName === "discover_tools") {
313
374
  const category = request.params.arguments?.category?.trim()?.toLowerCase();
@@ -421,18 +482,9 @@ function createServer() {
421
482
  const { _confirmed, ...cleanArgs } = request.params.arguments || {};
422
483
  request.params.arguments = cleanArgs;
423
484
  }
424
- if ((REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) && sessionId && authBySession[sessionId]) {
425
- const authData = authBySession[sessionId];
426
- const sessionContext = {
427
- sessionId,
428
- header: authData.header,
429
- token: authData.token,
430
- lastUsed: authData.lastUsed,
431
- apiUrl: authData.apiUrl,
432
- publicBaseUrl: authData.publicBaseUrl,
433
- allowedProjectIds: authData.allowedProjectIds,
434
- };
435
- // Run the handler within the retrieved context
485
+ if (sessionContext) {
486
+ // Policy selection and the tool handler must use the same request-local
487
+ // API URL and project allowlist.
436
488
  const result = await sessionAuthStore.run(sessionContext, () => handleToolCall(request.params));
437
489
  return logCompletion(result);
438
490
  }
@@ -1248,13 +1300,17 @@ async function handleGitLabError(response) {
1248
1300
  const errorBody = await response.text();
1249
1301
  // Check specifically for Rate Limit error
1250
1302
  if (response.status === 403 && errorBody.includes("User API Key Rate limit exceeded")) {
1251
- logger.error({ err: errorBody }, "GitLab API Rate Limit Exceeded");
1303
+ // Do not log the upstream body here; it is masked on the error object
1304
+ // before the common tool-call boundary returns/logs it.
1305
+ logger.error("GitLab API Rate Limit Exceeded");
1252
1306
  logger.error("User API Key Rate limit exceeded. Please try again later.");
1253
- throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
1307
+ const error = new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
1308
+ throw error;
1254
1309
  }
1255
1310
  else {
1256
1311
  // Handle other API errors
1257
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1312
+ const error = new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1313
+ throw error;
1258
1314
  }
1259
1315
  }
1260
1316
  }
@@ -1450,23 +1506,26 @@ async function listIssues(projectId, options = {}) {
1450
1506
  }
1451
1507
  // Add all query parameters
1452
1508
  Object.entries(options).forEach(([key, value]) => {
1453
- if (value !== undefined) {
1454
- const keys = ["labels", "assignee_username"];
1455
- if (keys.includes(key)) {
1456
- if (Array.isArray(value)) {
1457
- // Handle array of labels
1458
- value.forEach(label => {
1459
- url.searchParams.append(`${key}[]`, label.toString());
1460
- });
1461
- }
1462
- else if (value) {
1463
- url.searchParams.append(`${key}[]`, value.toString());
1464
- }
1509
+ // Drop blank entries inside arrays so ["", "bug"] behaves like the scalar guard below
1510
+ const normalized = dropBlankArrayEntries(value);
1511
+ if (isBlankFilterValue(normalized)) {
1512
+ return;
1513
+ }
1514
+ const keys = ["labels", "assignee_username"];
1515
+ if (keys.includes(key)) {
1516
+ if (Array.isArray(normalized)) {
1517
+ // Handle array of labels
1518
+ normalized.forEach(label => {
1519
+ url.searchParams.append(`${key}[]`, String(label));
1520
+ });
1465
1521
  }
1466
1522
  else {
1467
- url.searchParams.append(key, String(value));
1523
+ url.searchParams.append(`${key}[]`, String(normalized));
1468
1524
  }
1469
1525
  }
1526
+ else {
1527
+ url.searchParams.append(key, String(normalized));
1528
+ }
1470
1529
  });
1471
1530
  const response = await fetch(url.toString(), {
1472
1531
  ...getFetchConfig(),
@@ -1477,11 +1536,7 @@ async function listIssues(projectId, options = {}) {
1477
1536
  }
1478
1537
  async function listTodos(options = {}) {
1479
1538
  const url = new URL(`${getEffectiveApiUrl()}/todos`);
1480
- Object.entries(options).forEach(([key, value]) => {
1481
- if (value !== undefined) {
1482
- url.searchParams.append(key, String(value));
1483
- }
1484
- });
1539
+ appendFilterParams(url.searchParams, options);
1485
1540
  const response = await fetch(url.toString(), {
1486
1541
  ...getFetchConfig(),
1487
1542
  });
@@ -1510,20 +1565,22 @@ async function markAllTodosDone() {
1510
1565
  */
1511
1566
  function appendMergeRequestFilters(url, options) {
1512
1567
  Object.entries(options).forEach(([key, value]) => {
1513
- if (value === undefined) {
1568
+ // Drop blank entries inside arrays so ["", "bug"] behaves like the scalar guard below
1569
+ const normalized = dropBlankArrayEntries(value);
1570
+ if (isBlankFilterValue(normalized)) {
1514
1571
  return;
1515
1572
  }
1516
- if (key === "labels" && Array.isArray(value)) {
1517
- url.searchParams.append(key, value.join(","));
1573
+ if (key === "labels" && Array.isArray(normalized)) {
1574
+ url.searchParams.append(key, normalized.join(","));
1518
1575
  }
1519
- else if (key === "approved_by_usernames" && Array.isArray(value)) {
1576
+ else if (key === "approved_by_usernames" && Array.isArray(normalized)) {
1520
1577
  // GitLab expects array-bracket form: approved_by_usernames[]=alice&approved_by_usernames[]=bob
1521
- for (const v of value) {
1578
+ for (const v of normalized) {
1522
1579
  url.searchParams.append(`${key}[]`, String(v));
1523
1580
  }
1524
1581
  }
1525
1582
  else {
1526
- url.searchParams.append(key, String(value));
1583
+ url.searchParams.append(key, String(normalized));
1527
1584
  }
1528
1585
  });
1529
1586
  }
@@ -3408,6 +3465,17 @@ async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
3408
3465
  const data = await response.json();
3409
3466
  return GitLabDiscussionNoteSchema.parse(data);
3410
3467
  }
3468
+ async function getMergeRequestDiscussion(projectId, mergeRequestIid, discussionId) {
3469
+ projectId = decodeURIComponent(projectId); // Decode project ID
3470
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
3471
+ const response = await fetch(url.toString(), {
3472
+ ...getFetchConfig(),
3473
+ method: "GET",
3474
+ });
3475
+ await handleGitLabError(response);
3476
+ const data = await response.json();
3477
+ return GitLabDiscussionSchema.parse(data);
3478
+ }
3411
3479
  async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by, per_page, page) {
3412
3480
  projectId = decodeURIComponent(projectId); // Decode project ID
3413
3481
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
@@ -3733,11 +3801,7 @@ async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3733
3801
  async function listMergeRequestPipelines(projectId, mergeRequestIid, options = {}) {
3734
3802
  projectId = decodeURIComponent(projectId);
3735
3803
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/pipelines`);
3736
- Object.entries(options).forEach(([key, value]) => {
3737
- if (value !== undefined) {
3738
- url.searchParams.append(key, value.toString());
3739
- }
3740
- });
3804
+ appendFilterParams(url.searchParams, options);
3741
3805
  const response = await fetch(url.toString(), {
3742
3806
  ...getFetchConfig(),
3743
3807
  });
@@ -4667,16 +4731,7 @@ async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, uni
4667
4731
  async function listProjects(options = {}) {
4668
4732
  // Construct the query parameters
4669
4733
  const params = new URLSearchParams();
4670
- for (const [key, value] of Object.entries(options)) {
4671
- if (value !== undefined && value !== null) {
4672
- if (typeof value === "boolean") {
4673
- params.append(key, value ? "true" : "false");
4674
- }
4675
- else {
4676
- params.append(key, String(value));
4677
- }
4678
- }
4679
- }
4734
+ appendFilterParams(params, options);
4680
4735
  // Make the API request
4681
4736
  const response = await fetch(`${getEffectiveApiUrl()}/projects?${params.toString()}`, {
4682
4737
  ...getFetchConfig(),
@@ -4699,16 +4754,7 @@ async function listLabels(projectId, options = {}) {
4699
4754
  // Construct the URL with project path
4700
4755
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`);
4701
4756
  // Add query parameters
4702
- Object.entries(options).forEach(([key, value]) => {
4703
- if (value !== undefined) {
4704
- if (typeof value === "boolean") {
4705
- url.searchParams.append(key, value ? "true" : "false");
4706
- }
4707
- else {
4708
- url.searchParams.append(key, String(value));
4709
- }
4710
- }
4711
- });
4757
+ appendFilterParams(url.searchParams, options);
4712
4758
  // Make the API request
4713
4759
  const response = await fetch(url.toString(), {
4714
4760
  ...getFetchConfig(),
@@ -5165,11 +5211,7 @@ async function listPipelines(projectId, options = {}) {
5165
5211
  projectId = decodeURIComponent(projectId); // Decode project ID
5166
5212
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines`);
5167
5213
  // Add all query parameters
5168
- Object.entries(options).forEach(([key, value]) => {
5169
- if (value !== undefined) {
5170
- url.searchParams.append(key, value.toString());
5171
- }
5172
- });
5214
+ appendFilterParams(url.searchParams, options);
5173
5215
  const response = await fetch(url.toString(), {
5174
5216
  ...getFetchConfig(),
5175
5217
  });
@@ -5243,11 +5285,7 @@ async function updatePipelineMetadata(projectId, pipelineId, name) {
5243
5285
  async function listDeployments(projectId, options = {}) {
5244
5286
  projectId = decodeURIComponent(projectId);
5245
5287
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments`);
5246
- Object.entries(options).forEach(([key, value]) => {
5247
- if (value !== undefined) {
5248
- url.searchParams.append(key, value.toString());
5249
- }
5250
- });
5288
+ appendFilterParams(url.searchParams, options);
5251
5289
  const response = await fetch(url.toString(), {
5252
5290
  ...getFetchConfig(),
5253
5291
  });
@@ -5299,11 +5337,7 @@ async function deploymentRequest(projectId, path = "", method = "GET", body, que
5299
5337
  async function listEnvironments(projectId, options = {}) {
5300
5338
  projectId = decodeURIComponent(projectId);
5301
5339
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments`);
5302
- Object.entries(options).forEach(([key, value]) => {
5303
- if (value !== undefined) {
5304
- url.searchParams.append(key, value.toString());
5305
- }
5306
- });
5340
+ appendFilterParams(url.searchParams, options);
5307
5341
  const response = await fetch(url.toString(), {
5308
5342
  ...getFetchConfig(),
5309
5343
  });
@@ -5373,16 +5407,7 @@ async function listPipelineJobs(projectId, pipelineId, options = {}) {
5373
5407
  projectId = decodeURIComponent(projectId); // Decode project ID
5374
5408
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/jobs`);
5375
5409
  // Add all query parameters
5376
- Object.entries(options).forEach(([key, value]) => {
5377
- if (value !== undefined) {
5378
- if (typeof value === "boolean") {
5379
- url.searchParams.append(key, value ? "true" : "false");
5380
- }
5381
- else {
5382
- url.searchParams.append(key, value.toString());
5383
- }
5384
- }
5385
- });
5410
+ appendFilterParams(url.searchParams, options);
5386
5411
  const response = await fetch(url.toString(), {
5387
5412
  ...getFetchConfig(),
5388
5413
  });
@@ -5405,16 +5430,7 @@ async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
5405
5430
  projectId = decodeURIComponent(projectId); // Decode project ID
5406
5431
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/bridges`);
5407
5432
  // Add all query parameters
5408
- Object.entries(options).forEach(([key, value]) => {
5409
- if (value !== undefined) {
5410
- if (typeof value === "boolean") {
5411
- url.searchParams.append(key, value ? "true" : "false");
5412
- }
5413
- else {
5414
- url.searchParams.append(key, value.toString());
5415
- }
5416
- }
5417
- });
5433
+ appendFilterParams(url.searchParams, options);
5418
5434
  const response = await fetch(url.toString(), {
5419
5435
  ...getFetchConfig(),
5420
5436
  });
@@ -5499,16 +5515,7 @@ async function validateCiLint(projectId, options) {
5499
5515
  async function validateProjectCiLint(projectId, options) {
5500
5516
  projectId = decodeURIComponent(projectId);
5501
5517
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/ci/lint`);
5502
- Object.entries(options).forEach(([key, value]) => {
5503
- if (value !== undefined) {
5504
- if (typeof value === "boolean") {
5505
- url.searchParams.append(key, value ? "true" : "false");
5506
- }
5507
- else {
5508
- url.searchParams.append(key, value.toString());
5509
- }
5510
- }
5511
- });
5518
+ appendFilterParams(url.searchParams, options);
5512
5519
  const response = await fetch(url.toString(), {
5513
5520
  ...getFetchConfig(),
5514
5521
  });
@@ -5527,16 +5534,7 @@ async function validateProjectCiLint(projectId, options) {
5527
5534
  async function listJobArtifacts(projectId, jobId, options = {}) {
5528
5535
  projectId = decodeURIComponent(projectId);
5529
5536
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/tree`);
5530
- Object.entries(options).forEach(([key, value]) => {
5531
- if (value !== undefined) {
5532
- if (typeof value === "boolean") {
5533
- url.searchParams.append(key, value ? "true" : "false");
5534
- }
5535
- else {
5536
- url.searchParams.append(key, value.toString());
5537
- }
5538
- }
5539
- });
5537
+ appendFilterParams(url.searchParams, options);
5540
5538
  const response = await fetch(url.toString(), {
5541
5539
  ...getFetchConfig(),
5542
5540
  });
@@ -5673,11 +5671,7 @@ async function cancelPipeline(projectId, pipelineId) {
5673
5671
  async function listPipelineSchedules(projectId, options = {}) {
5674
5672
  projectId = decodeURIComponent(projectId); // Decode project ID
5675
5673
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5676
- Object.entries(options).forEach(([key, value]) => {
5677
- if (value !== undefined) {
5678
- url.searchParams.append(key, value.toString());
5679
- }
5680
- });
5674
+ appendFilterParams(url.searchParams, options);
5681
5675
  const response = await fetch(url.toString(), {
5682
5676
  ...getFetchConfig(),
5683
5677
  });
@@ -5713,11 +5707,7 @@ async function getPipelineSchedule(projectId, pipelineScheduleId) {
5713
5707
  async function listPipelineSchedulePipelines(projectId, pipelineScheduleId, options = {}) {
5714
5708
  projectId = decodeURIComponent(projectId); // Decode project ID
5715
5709
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/pipelines`);
5716
- Object.entries(options).forEach(([key, value]) => {
5717
- if (value !== undefined) {
5718
- url.searchParams.append(key, value.toString());
5719
- }
5720
- });
5710
+ appendFilterParams(url.searchParams, options);
5721
5711
  const response = await fetch(url.toString(), {
5722
5712
  ...getFetchConfig(),
5723
5713
  });
@@ -6040,15 +6030,18 @@ async function listProjectMilestones(projectId, options) {
6040
6030
  projectId = decodeURIComponent(projectId);
6041
6031
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`);
6042
6032
  Object.entries(options).forEach(([key, value]) => {
6043
- if (value !== undefined) {
6044
- if (key === "iids" && Array.isArray(value) && value.length > 0) {
6045
- value.forEach(iid => {
6046
- url.searchParams.append("iids[]", iid.toString());
6047
- });
6048
- }
6049
- else if (value !== undefined) {
6050
- url.searchParams.append(key, value.toString());
6051
- }
6033
+ // Drop blank entries inside arrays so they behave like the scalar blank guard below
6034
+ const normalized = dropBlankArrayEntries(value);
6035
+ if (isBlankFilterValue(normalized)) {
6036
+ return;
6037
+ }
6038
+ if (key === "iids" && Array.isArray(normalized)) {
6039
+ normalized.forEach(iid => {
6040
+ url.searchParams.append("iids[]", String(iid));
6041
+ });
6042
+ }
6043
+ else {
6044
+ url.searchParams.append(key, String(normalized));
6052
6045
  }
6053
6046
  });
6054
6047
  const response = await fetch(url.toString(), {
@@ -6135,11 +6128,7 @@ async function deleteProjectMilestone(projectId, milestoneId) {
6135
6128
  async function getMilestoneIssues(projectId, milestoneId, options = {}) {
6136
6129
  projectId = decodeURIComponent(projectId);
6137
6130
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6138
- Object.entries(options).forEach(([key, value]) => {
6139
- if (value !== undefined) {
6140
- url.searchParams.append(key, String(value));
6141
- }
6142
- });
6131
+ appendFilterParams(url.searchParams, options);
6143
6132
  const response = await fetch(url.toString(), {
6144
6133
  ...getFetchConfig(),
6145
6134
  });
@@ -6156,11 +6145,7 @@ async function getMilestoneIssues(projectId, milestoneId, options = {}) {
6156
6145
  async function getMilestoneMergeRequests(projectId, milestoneId, options = {}) {
6157
6146
  projectId = decodeURIComponent(projectId);
6158
6147
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6159
- Object.entries(options).forEach(([key, value]) => {
6160
- if (value !== undefined) {
6161
- url.searchParams.append(key, String(value));
6162
- }
6163
- });
6148
+ appendFilterParams(url.searchParams, options);
6164
6149
  const response = await fetch(url.toString(), {
6165
6150
  ...getFetchConfig(),
6166
6151
  });
@@ -6194,11 +6179,7 @@ async function promoteProjectMilestone(projectId, milestoneId) {
6194
6179
  async function getMilestoneBurndownEvents(projectId, milestoneId, options = {}) {
6195
6180
  projectId = decodeURIComponent(projectId);
6196
6181
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6197
- Object.entries(options).forEach(([key, value]) => {
6198
- if (value !== undefined) {
6199
- url.searchParams.append(key, String(value));
6200
- }
6201
- });
6182
+ appendFilterParams(url.searchParams, options);
6202
6183
  const response = await fetch(url.toString(), {
6203
6184
  ...getFetchConfig(),
6204
6185
  });
@@ -6213,15 +6194,18 @@ async function listGroupMilestones(groupId, options) {
6213
6194
  groupId = decodeURIComponent(groupId);
6214
6195
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones`);
6215
6196
  Object.entries(options).forEach(([key, value]) => {
6216
- if (value !== undefined) {
6217
- if (key === "iids" && Array.isArray(value) && value.length > 0) {
6218
- value.forEach(iid => {
6219
- url.searchParams.append("iids[]", iid.toString());
6220
- });
6221
- }
6222
- else {
6223
- url.searchParams.append(key, value.toString());
6224
- }
6197
+ // Drop blank entries inside arrays so they behave like the scalar blank guard below
6198
+ const normalized = dropBlankArrayEntries(value);
6199
+ if (isBlankFilterValue(normalized)) {
6200
+ return;
6201
+ }
6202
+ if (key === "iids" && Array.isArray(normalized)) {
6203
+ normalized.forEach(iid => {
6204
+ url.searchParams.append("iids[]", String(iid));
6205
+ });
6206
+ }
6207
+ else {
6208
+ url.searchParams.append(key, String(normalized));
6225
6209
  }
6226
6210
  });
6227
6211
  const response = await fetch(url.toString(), {
@@ -6292,11 +6276,7 @@ async function deleteGroupMilestone(groupId, milestoneId) {
6292
6276
  async function getGroupMilestoneIssues(groupId, milestoneId, options = {}) {
6293
6277
  groupId = decodeURIComponent(groupId);
6294
6278
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6295
- Object.entries(options).forEach(([key, value]) => {
6296
- if (value !== undefined) {
6297
- url.searchParams.append(key, String(value));
6298
- }
6299
- });
6279
+ appendFilterParams(url.searchParams, options);
6300
6280
  const response = await fetch(url.toString(), {
6301
6281
  ...getFetchConfig(),
6302
6282
  });
@@ -6310,11 +6290,7 @@ async function getGroupMilestoneIssues(groupId, milestoneId, options = {}) {
6310
6290
  async function getGroupMilestoneMergeRequests(groupId, milestoneId, options = {}) {
6311
6291
  groupId = decodeURIComponent(groupId);
6312
6292
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6313
- Object.entries(options).forEach(([key, value]) => {
6314
- if (value !== undefined) {
6315
- url.searchParams.append(key, String(value));
6316
- }
6317
- });
6293
+ appendFilterParams(url.searchParams, options);
6318
6294
  const response = await fetch(url.toString(), {
6319
6295
  ...getFetchConfig(),
6320
6296
  });
@@ -6328,11 +6304,7 @@ async function getGroupMilestoneMergeRequests(groupId, milestoneId, options = {}
6328
6304
  async function getGroupMilestoneBurndownEvents(groupId, milestoneId, options = {}) {
6329
6305
  groupId = decodeURIComponent(groupId);
6330
6306
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6331
- Object.entries(options).forEach(([key, value]) => {
6332
- if (value !== undefined) {
6333
- url.searchParams.append(key, String(value));
6334
- }
6335
- });
6307
+ appendFilterParams(url.searchParams, options);
6336
6308
  const response = await fetch(url.toString(), {
6337
6309
  ...getFetchConfig(),
6338
6310
  });
@@ -6531,11 +6503,7 @@ async function getFileBlame(projectId, options) {
6531
6503
  async function listCommitStatuses(projectId, sha, options = {}) {
6532
6504
  projectId = decodeURIComponent(projectId);
6533
6505
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}/statuses`);
6534
- Object.entries(options).forEach(([key, value]) => {
6535
- if (value !== undefined) {
6536
- url.searchParams.append(key, value.toString());
6537
- }
6538
- });
6506
+ appendFilterParams(url.searchParams, options);
6539
6507
  const response = await fetch(url.toString(), {
6540
6508
  ...getFetchConfig(),
6541
6509
  });
@@ -6557,11 +6525,7 @@ async function createCommitStatus(projectId, sha, options) {
6557
6525
  }
6558
6526
  projectId = decodeURIComponent(projectId);
6559
6527
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/statuses/${encodeURIComponent(sha)}`);
6560
- Object.entries(options).forEach(([key, value]) => {
6561
- if (value !== undefined) {
6562
- url.searchParams.append(key, value.toString());
6563
- }
6564
- });
6528
+ appendFilterParams(url.searchParams, options);
6565
6529
  const response = await fetch(url.toString(), {
6566
6530
  ...getFetchConfig(),
6567
6531
  method: "POST",
@@ -6739,19 +6703,13 @@ async function listProjectVariables(projectId, options = {}) {
6739
6703
  projectId = decodeURIComponent(projectId);
6740
6704
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables`);
6741
6705
  Object.entries(options).forEach(([key, value]) => {
6742
- if (value === undefined)
6743
- return;
6744
6706
  if (key === "filter" && typeof value === "object" && value !== null) {
6745
- Object.entries(value).forEach(([fKey, fVal]) => {
6746
- url.searchParams.append(`filter[${fKey}]`, fVal);
6707
+ Object.entries(value).forEach(([filterKey, filterValue]) => {
6708
+ appendFilterParam(url.searchParams, `filter[${filterKey}]`, filterValue);
6747
6709
  });
6710
+ return;
6748
6711
  }
6749
- else if (typeof value === "boolean") {
6750
- url.searchParams.append(key, value ? "true" : "false");
6751
- }
6752
- else {
6753
- url.searchParams.append(key, String(value));
6754
- }
6712
+ appendFilterParam(url.searchParams, key, value);
6755
6713
  });
6756
6714
  const response = await fetch(url.toString(), getFetchConfig());
6757
6715
  await handleGitLabError(response);
@@ -6805,19 +6763,13 @@ async function listGroupVariables(groupId, options = {}) {
6805
6763
  const encoded = encodeURIComponent(decodeURIComponent(groupId));
6806
6764
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables`);
6807
6765
  Object.entries(options).forEach(([key, value]) => {
6808
- if (value === undefined)
6809
- return;
6810
6766
  if (key === "filter" && typeof value === "object" && value !== null) {
6811
- Object.entries(value).forEach(([fKey, fVal]) => {
6812
- url.searchParams.append(`filter[${fKey}]`, fVal);
6767
+ Object.entries(value).forEach(([filterKey, filterValue]) => {
6768
+ appendFilterParam(url.searchParams, `filter[${filterKey}]`, filterValue);
6813
6769
  });
6770
+ return;
6814
6771
  }
6815
- else if (typeof value === "boolean") {
6816
- url.searchParams.append(key, value ? "true" : "false");
6817
- }
6818
- else {
6819
- url.searchParams.append(key, String(value));
6820
- }
6772
+ appendFilterParam(url.searchParams, key, value);
6821
6773
  });
6822
6774
  const response = await fetch(url.toString(), getFetchConfig());
6823
6775
  await handleGitLabError(response);
@@ -7364,11 +7316,7 @@ async function downloadAttachment(projectId, secret, filename, localPath) {
7364
7316
  async function listEvents(options = {}) {
7365
7317
  const url = new URL(`${getEffectiveApiUrl()}/events`);
7366
7318
  // Add all query parameters
7367
- Object.entries(options).forEach(([key, value]) => {
7368
- if (value !== undefined) {
7369
- url.searchParams.append(key, value.toString());
7370
- }
7371
- });
7319
+ appendFilterParams(url.searchParams, options);
7372
7320
  const response = await fetch(url.toString(), { ...getFetchConfig() });
7373
7321
  if (!response.ok) {
7374
7322
  await handleGitLabError(response);
@@ -7386,11 +7334,7 @@ async function getProjectEvents(projectId, options = {}) {
7386
7334
  const effectiveProjectId = getEffectiveProjectId(projectId);
7387
7335
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/events`);
7388
7336
  // Add all query parameters
7389
- Object.entries(options).forEach(([key, value]) => {
7390
- if (value !== undefined) {
7391
- url.searchParams.append(key, value.toString());
7392
- }
7393
- });
7337
+ appendFilterParams(url.searchParams, options);
7394
7338
  const response = await fetch(url.toString(), { ...getFetchConfig() });
7395
7339
  if (!response.ok) {
7396
7340
  await handleGitLabError(response);
@@ -7409,11 +7353,7 @@ async function listReleases(projectId, options = {}) {
7409
7353
  const effectiveProjectId = getEffectiveProjectId(projectId);
7410
7354
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases`);
7411
7355
  // Add query parameters
7412
- Object.entries(options).forEach(([key, value]) => {
7413
- if (value !== undefined) {
7414
- url.searchParams.append(key, value.toString());
7415
- }
7416
- });
7356
+ appendFilterParams(url.searchParams, options);
7417
7357
  const response = await fetch(url.toString(), {
7418
7358
  ...getFetchConfig(),
7419
7359
  });
@@ -7536,11 +7476,7 @@ async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
7536
7476
  async function listTags(projectId, options = {}) {
7537
7477
  const effectiveProjectId = getEffectiveProjectId(projectId);
7538
7478
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags`);
7539
- Object.entries(options).forEach(([key, value]) => {
7540
- if (value !== undefined) {
7541
- url.searchParams.append(key, value.toString());
7542
- }
7543
- });
7479
+ appendFilterParams(url.searchParams, options);
7544
7480
  const response = await fetch(url.toString(), {
7545
7481
  ...getFetchConfig(),
7546
7482
  });
@@ -7957,6 +7893,13 @@ async function handleToolCall(params) {
7957
7893
  content: [{ type: "text", text: JSON.stringify(note) }],
7958
7894
  };
7959
7895
  }
7896
+ case "get_merge_request_discussion": {
7897
+ const args = GetMergeRequestDiscussionSchema.parse(params.arguments);
7898
+ const discussion = await getMergeRequestDiscussion(args.project_id, args.merge_request_iid, args.discussion_id);
7899
+ return {
7900
+ content: [{ type: "text", text: JSON.stringify(discussion) }],
7901
+ };
7902
+ }
7960
7903
  case "get_merge_request_notes": {
7961
7904
  const args = GetMergeRequestNotesSchema.parse(params.arguments);
7962
7905
  const notes = await getMergeRequestNotes(args.project_id, args.merge_request_iid, args.sort, args.order_by, args.per_page, args.page);
@@ -8773,8 +8716,8 @@ async function handleToolCall(params) {
8773
8716
  };
8774
8717
  }
8775
8718
  case "list_labels": {
8776
- const args = ListLabelsSchema.parse(params.arguments);
8777
- const labels = await listLabels(args.project_id, args);
8719
+ const { project_id, ...options } = ListLabelsSchema.parse(params.arguments);
8720
+ const labels = await listLabels(project_id, options);
8778
8721
  return {
8779
8722
  content: [{ type: "text", text: JSON.stringify(labels) }],
8780
8723
  };