@zereight/mcp-gitlab 2.1.63 → 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/README.ko.md CHANGED
@@ -110,7 +110,7 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
110
110
 
111
111
  예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
112
112
 
113
- 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.62`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
113
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.63`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
114
114
 
115
115
  #### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
116
116
 
package/README.md CHANGED
@@ -115,7 +115,7 @@ The store path is pinned by your lock file; update it with `nix flake update git
115
115
 
116
116
  The examples use `zereight-mcp-gitlab`, a less collision-prone alias for the legacy `mcp-gitlab` binary. If your MCP client cannot find it, use the absolute path from `which zereight-mcp-gitlab`.
117
117
 
118
- No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.62`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
118
+ No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.63`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
119
119
 
120
120
  #### Using CLI Arguments (for clients with env var issues)
121
121
 
package/README.zh-CN.md CHANGED
@@ -110,7 +110,7 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
110
110
 
111
111
  示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
112
112
 
113
- 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.62`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
113
+ 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.63`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
114
114
 
115
115
  #### 使用 CLI 参数(适用于环境变量有问题的客户端)
116
116
 
package/build/index.js CHANGED
@@ -100,7 +100,7 @@ import { checkForNewVersion } from "./utils/version-check.js";
100
100
  import { assertGitLabVersionAtLeast } from "./utils/gitlab-version-gate.js";
101
101
  import { parseGitLabVersionApiResponse, } from "./utils/gitlab-instance-version.js";
102
102
  import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
103
- 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";
104
104
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
105
105
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
106
106
  import { GitLabClientPool } from "./gitlab-client-pool.js";
@@ -1506,23 +1506,26 @@ async function listIssues(projectId, options = {}) {
1506
1506
  }
1507
1507
  // Add all query parameters
1508
1508
  Object.entries(options).forEach(([key, value]) => {
1509
- if (value !== undefined) {
1510
- const keys = ["labels", "assignee_username"];
1511
- if (keys.includes(key)) {
1512
- if (Array.isArray(value)) {
1513
- // Handle array of labels
1514
- value.forEach(label => {
1515
- url.searchParams.append(`${key}[]`, label.toString());
1516
- });
1517
- }
1518
- else if (value) {
1519
- url.searchParams.append(`${key}[]`, value.toString());
1520
- }
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
+ });
1521
1521
  }
1522
1522
  else {
1523
- url.searchParams.append(key, String(value));
1523
+ url.searchParams.append(`${key}[]`, String(normalized));
1524
1524
  }
1525
1525
  }
1526
+ else {
1527
+ url.searchParams.append(key, String(normalized));
1528
+ }
1526
1529
  });
1527
1530
  const response = await fetch(url.toString(), {
1528
1531
  ...getFetchConfig(),
@@ -1533,11 +1536,7 @@ async function listIssues(projectId, options = {}) {
1533
1536
  }
1534
1537
  async function listTodos(options = {}) {
1535
1538
  const url = new URL(`${getEffectiveApiUrl()}/todos`);
1536
- Object.entries(options).forEach(([key, value]) => {
1537
- if (value !== undefined) {
1538
- url.searchParams.append(key, String(value));
1539
- }
1540
- });
1539
+ appendFilterParams(url.searchParams, options);
1541
1540
  const response = await fetch(url.toString(), {
1542
1541
  ...getFetchConfig(),
1543
1542
  });
@@ -1566,20 +1565,22 @@ async function markAllTodosDone() {
1566
1565
  */
1567
1566
  function appendMergeRequestFilters(url, options) {
1568
1567
  Object.entries(options).forEach(([key, value]) => {
1569
- 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)) {
1570
1571
  return;
1571
1572
  }
1572
- if (key === "labels" && Array.isArray(value)) {
1573
- url.searchParams.append(key, value.join(","));
1573
+ if (key === "labels" && Array.isArray(normalized)) {
1574
+ url.searchParams.append(key, normalized.join(","));
1574
1575
  }
1575
- else if (key === "approved_by_usernames" && Array.isArray(value)) {
1576
+ else if (key === "approved_by_usernames" && Array.isArray(normalized)) {
1576
1577
  // GitLab expects array-bracket form: approved_by_usernames[]=alice&approved_by_usernames[]=bob
1577
- for (const v of value) {
1578
+ for (const v of normalized) {
1578
1579
  url.searchParams.append(`${key}[]`, String(v));
1579
1580
  }
1580
1581
  }
1581
1582
  else {
1582
- url.searchParams.append(key, String(value));
1583
+ url.searchParams.append(key, String(normalized));
1583
1584
  }
1584
1585
  });
1585
1586
  }
@@ -3800,11 +3801,7 @@ async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3800
3801
  async function listMergeRequestPipelines(projectId, mergeRequestIid, options = {}) {
3801
3802
  projectId = decodeURIComponent(projectId);
3802
3803
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/pipelines`);
3803
- Object.entries(options).forEach(([key, value]) => {
3804
- if (value !== undefined) {
3805
- url.searchParams.append(key, value.toString());
3806
- }
3807
- });
3804
+ appendFilterParams(url.searchParams, options);
3808
3805
  const response = await fetch(url.toString(), {
3809
3806
  ...getFetchConfig(),
3810
3807
  });
@@ -4734,16 +4731,7 @@ async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, uni
4734
4731
  async function listProjects(options = {}) {
4735
4732
  // Construct the query parameters
4736
4733
  const params = new URLSearchParams();
4737
- for (const [key, value] of Object.entries(options)) {
4738
- if (value !== undefined && value !== null) {
4739
- if (typeof value === "boolean") {
4740
- params.append(key, value ? "true" : "false");
4741
- }
4742
- else {
4743
- params.append(key, String(value));
4744
- }
4745
- }
4746
- }
4734
+ appendFilterParams(params, options);
4747
4735
  // Make the API request
4748
4736
  const response = await fetch(`${getEffectiveApiUrl()}/projects?${params.toString()}`, {
4749
4737
  ...getFetchConfig(),
@@ -4766,16 +4754,7 @@ async function listLabels(projectId, options = {}) {
4766
4754
  // Construct the URL with project path
4767
4755
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/labels`);
4768
4756
  // Add query parameters
4769
- Object.entries(options).forEach(([key, value]) => {
4770
- if (value !== undefined) {
4771
- if (typeof value === "boolean") {
4772
- url.searchParams.append(key, value ? "true" : "false");
4773
- }
4774
- else {
4775
- url.searchParams.append(key, String(value));
4776
- }
4777
- }
4778
- });
4757
+ appendFilterParams(url.searchParams, options);
4779
4758
  // Make the API request
4780
4759
  const response = await fetch(url.toString(), {
4781
4760
  ...getFetchConfig(),
@@ -5232,11 +5211,7 @@ async function listPipelines(projectId, options = {}) {
5232
5211
  projectId = decodeURIComponent(projectId); // Decode project ID
5233
5212
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines`);
5234
5213
  // Add all query parameters
5235
- Object.entries(options).forEach(([key, value]) => {
5236
- if (value !== undefined) {
5237
- url.searchParams.append(key, value.toString());
5238
- }
5239
- });
5214
+ appendFilterParams(url.searchParams, options);
5240
5215
  const response = await fetch(url.toString(), {
5241
5216
  ...getFetchConfig(),
5242
5217
  });
@@ -5310,11 +5285,7 @@ async function updatePipelineMetadata(projectId, pipelineId, name) {
5310
5285
  async function listDeployments(projectId, options = {}) {
5311
5286
  projectId = decodeURIComponent(projectId);
5312
5287
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments`);
5313
- Object.entries(options).forEach(([key, value]) => {
5314
- if (value !== undefined) {
5315
- url.searchParams.append(key, value.toString());
5316
- }
5317
- });
5288
+ appendFilterParams(url.searchParams, options);
5318
5289
  const response = await fetch(url.toString(), {
5319
5290
  ...getFetchConfig(),
5320
5291
  });
@@ -5366,11 +5337,7 @@ async function deploymentRequest(projectId, path = "", method = "GET", body, que
5366
5337
  async function listEnvironments(projectId, options = {}) {
5367
5338
  projectId = decodeURIComponent(projectId);
5368
5339
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments`);
5369
- Object.entries(options).forEach(([key, value]) => {
5370
- if (value !== undefined) {
5371
- url.searchParams.append(key, value.toString());
5372
- }
5373
- });
5340
+ appendFilterParams(url.searchParams, options);
5374
5341
  const response = await fetch(url.toString(), {
5375
5342
  ...getFetchConfig(),
5376
5343
  });
@@ -5440,16 +5407,7 @@ async function listPipelineJobs(projectId, pipelineId, options = {}) {
5440
5407
  projectId = decodeURIComponent(projectId); // Decode project ID
5441
5408
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/jobs`);
5442
5409
  // Add all query parameters
5443
- Object.entries(options).forEach(([key, value]) => {
5444
- if (value !== undefined) {
5445
- if (typeof value === "boolean") {
5446
- url.searchParams.append(key, value ? "true" : "false");
5447
- }
5448
- else {
5449
- url.searchParams.append(key, value.toString());
5450
- }
5451
- }
5452
- });
5410
+ appendFilterParams(url.searchParams, options);
5453
5411
  const response = await fetch(url.toString(), {
5454
5412
  ...getFetchConfig(),
5455
5413
  });
@@ -5472,16 +5430,7 @@ async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
5472
5430
  projectId = decodeURIComponent(projectId); // Decode project ID
5473
5431
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/bridges`);
5474
5432
  // Add all query parameters
5475
- Object.entries(options).forEach(([key, value]) => {
5476
- if (value !== undefined) {
5477
- if (typeof value === "boolean") {
5478
- url.searchParams.append(key, value ? "true" : "false");
5479
- }
5480
- else {
5481
- url.searchParams.append(key, value.toString());
5482
- }
5483
- }
5484
- });
5433
+ appendFilterParams(url.searchParams, options);
5485
5434
  const response = await fetch(url.toString(), {
5486
5435
  ...getFetchConfig(),
5487
5436
  });
@@ -5566,16 +5515,7 @@ async function validateCiLint(projectId, options) {
5566
5515
  async function validateProjectCiLint(projectId, options) {
5567
5516
  projectId = decodeURIComponent(projectId);
5568
5517
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/ci/lint`);
5569
- Object.entries(options).forEach(([key, value]) => {
5570
- if (value !== undefined) {
5571
- if (typeof value === "boolean") {
5572
- url.searchParams.append(key, value ? "true" : "false");
5573
- }
5574
- else {
5575
- url.searchParams.append(key, value.toString());
5576
- }
5577
- }
5578
- });
5518
+ appendFilterParams(url.searchParams, options);
5579
5519
  const response = await fetch(url.toString(), {
5580
5520
  ...getFetchConfig(),
5581
5521
  });
@@ -5594,16 +5534,7 @@ async function validateProjectCiLint(projectId, options) {
5594
5534
  async function listJobArtifacts(projectId, jobId, options = {}) {
5595
5535
  projectId = decodeURIComponent(projectId);
5596
5536
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/tree`);
5597
- Object.entries(options).forEach(([key, value]) => {
5598
- if (value !== undefined) {
5599
- if (typeof value === "boolean") {
5600
- url.searchParams.append(key, value ? "true" : "false");
5601
- }
5602
- else {
5603
- url.searchParams.append(key, value.toString());
5604
- }
5605
- }
5606
- });
5537
+ appendFilterParams(url.searchParams, options);
5607
5538
  const response = await fetch(url.toString(), {
5608
5539
  ...getFetchConfig(),
5609
5540
  });
@@ -5740,11 +5671,7 @@ async function cancelPipeline(projectId, pipelineId) {
5740
5671
  async function listPipelineSchedules(projectId, options = {}) {
5741
5672
  projectId = decodeURIComponent(projectId); // Decode project ID
5742
5673
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules`);
5743
- Object.entries(options).forEach(([key, value]) => {
5744
- if (value !== undefined) {
5745
- url.searchParams.append(key, value.toString());
5746
- }
5747
- });
5674
+ appendFilterParams(url.searchParams, options);
5748
5675
  const response = await fetch(url.toString(), {
5749
5676
  ...getFetchConfig(),
5750
5677
  });
@@ -5780,11 +5707,7 @@ async function getPipelineSchedule(projectId, pipelineScheduleId) {
5780
5707
  async function listPipelineSchedulePipelines(projectId, pipelineScheduleId, options = {}) {
5781
5708
  projectId = decodeURIComponent(projectId); // Decode project ID
5782
5709
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline_schedules/${encodeGitLabPathSegment(pipelineScheduleId)}/pipelines`);
5783
- Object.entries(options).forEach(([key, value]) => {
5784
- if (value !== undefined) {
5785
- url.searchParams.append(key, value.toString());
5786
- }
5787
- });
5710
+ appendFilterParams(url.searchParams, options);
5788
5711
  const response = await fetch(url.toString(), {
5789
5712
  ...getFetchConfig(),
5790
5713
  });
@@ -6107,15 +6030,18 @@ async function listProjectMilestones(projectId, options) {
6107
6030
  projectId = decodeURIComponent(projectId);
6108
6031
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones`);
6109
6032
  Object.entries(options).forEach(([key, value]) => {
6110
- if (value !== undefined) {
6111
- if (key === "iids" && Array.isArray(value) && value.length > 0) {
6112
- value.forEach(iid => {
6113
- url.searchParams.append("iids[]", iid.toString());
6114
- });
6115
- }
6116
- else if (value !== undefined) {
6117
- url.searchParams.append(key, value.toString());
6118
- }
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));
6119
6045
  }
6120
6046
  });
6121
6047
  const response = await fetch(url.toString(), {
@@ -6202,11 +6128,7 @@ async function deleteProjectMilestone(projectId, milestoneId) {
6202
6128
  async function getMilestoneIssues(projectId, milestoneId, options = {}) {
6203
6129
  projectId = decodeURIComponent(projectId);
6204
6130
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6205
- Object.entries(options).forEach(([key, value]) => {
6206
- if (value !== undefined) {
6207
- url.searchParams.append(key, String(value));
6208
- }
6209
- });
6131
+ appendFilterParams(url.searchParams, options);
6210
6132
  const response = await fetch(url.toString(), {
6211
6133
  ...getFetchConfig(),
6212
6134
  });
@@ -6223,11 +6145,7 @@ async function getMilestoneIssues(projectId, milestoneId, options = {}) {
6223
6145
  async function getMilestoneMergeRequests(projectId, milestoneId, options = {}) {
6224
6146
  projectId = decodeURIComponent(projectId);
6225
6147
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6226
- Object.entries(options).forEach(([key, value]) => {
6227
- if (value !== undefined) {
6228
- url.searchParams.append(key, String(value));
6229
- }
6230
- });
6148
+ appendFilterParams(url.searchParams, options);
6231
6149
  const response = await fetch(url.toString(), {
6232
6150
  ...getFetchConfig(),
6233
6151
  });
@@ -6261,11 +6179,7 @@ async function promoteProjectMilestone(projectId, milestoneId) {
6261
6179
  async function getMilestoneBurndownEvents(projectId, milestoneId, options = {}) {
6262
6180
  projectId = decodeURIComponent(projectId);
6263
6181
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6264
- Object.entries(options).forEach(([key, value]) => {
6265
- if (value !== undefined) {
6266
- url.searchParams.append(key, String(value));
6267
- }
6268
- });
6182
+ appendFilterParams(url.searchParams, options);
6269
6183
  const response = await fetch(url.toString(), {
6270
6184
  ...getFetchConfig(),
6271
6185
  });
@@ -6280,15 +6194,18 @@ async function listGroupMilestones(groupId, options) {
6280
6194
  groupId = decodeURIComponent(groupId);
6281
6195
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones`);
6282
6196
  Object.entries(options).forEach(([key, value]) => {
6283
- if (value !== undefined) {
6284
- if (key === "iids" && Array.isArray(value) && value.length > 0) {
6285
- value.forEach(iid => {
6286
- url.searchParams.append("iids[]", iid.toString());
6287
- });
6288
- }
6289
- else {
6290
- url.searchParams.append(key, value.toString());
6291
- }
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));
6292
6209
  }
6293
6210
  });
6294
6211
  const response = await fetch(url.toString(), {
@@ -6359,11 +6276,7 @@ async function deleteGroupMilestone(groupId, milestoneId) {
6359
6276
  async function getGroupMilestoneIssues(groupId, milestoneId, options = {}) {
6360
6277
  groupId = decodeURIComponent(groupId);
6361
6278
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
6362
- Object.entries(options).forEach(([key, value]) => {
6363
- if (value !== undefined) {
6364
- url.searchParams.append(key, String(value));
6365
- }
6366
- });
6279
+ appendFilterParams(url.searchParams, options);
6367
6280
  const response = await fetch(url.toString(), {
6368
6281
  ...getFetchConfig(),
6369
6282
  });
@@ -6377,11 +6290,7 @@ async function getGroupMilestoneIssues(groupId, milestoneId, options = {}) {
6377
6290
  async function getGroupMilestoneMergeRequests(groupId, milestoneId, options = {}) {
6378
6291
  groupId = decodeURIComponent(groupId);
6379
6292
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
6380
- Object.entries(options).forEach(([key, value]) => {
6381
- if (value !== undefined) {
6382
- url.searchParams.append(key, String(value));
6383
- }
6384
- });
6293
+ appendFilterParams(url.searchParams, options);
6385
6294
  const response = await fetch(url.toString(), {
6386
6295
  ...getFetchConfig(),
6387
6296
  });
@@ -6395,11 +6304,7 @@ async function getGroupMilestoneMergeRequests(groupId, milestoneId, options = {}
6395
6304
  async function getGroupMilestoneBurndownEvents(groupId, milestoneId, options = {}) {
6396
6305
  groupId = decodeURIComponent(groupId);
6397
6306
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
6398
- Object.entries(options).forEach(([key, value]) => {
6399
- if (value !== undefined) {
6400
- url.searchParams.append(key, String(value));
6401
- }
6402
- });
6307
+ appendFilterParams(url.searchParams, options);
6403
6308
  const response = await fetch(url.toString(), {
6404
6309
  ...getFetchConfig(),
6405
6310
  });
@@ -6598,11 +6503,7 @@ async function getFileBlame(projectId, options) {
6598
6503
  async function listCommitStatuses(projectId, sha, options = {}) {
6599
6504
  projectId = decodeURIComponent(projectId);
6600
6505
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}/statuses`);
6601
- Object.entries(options).forEach(([key, value]) => {
6602
- if (value !== undefined) {
6603
- url.searchParams.append(key, value.toString());
6604
- }
6605
- });
6506
+ appendFilterParams(url.searchParams, options);
6606
6507
  const response = await fetch(url.toString(), {
6607
6508
  ...getFetchConfig(),
6608
6509
  });
@@ -6624,11 +6525,7 @@ async function createCommitStatus(projectId, sha, options) {
6624
6525
  }
6625
6526
  projectId = decodeURIComponent(projectId);
6626
6527
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/statuses/${encodeURIComponent(sha)}`);
6627
- Object.entries(options).forEach(([key, value]) => {
6628
- if (value !== undefined) {
6629
- url.searchParams.append(key, value.toString());
6630
- }
6631
- });
6528
+ appendFilterParams(url.searchParams, options);
6632
6529
  const response = await fetch(url.toString(), {
6633
6530
  ...getFetchConfig(),
6634
6531
  method: "POST",
@@ -6806,19 +6703,13 @@ async function listProjectVariables(projectId, options = {}) {
6806
6703
  projectId = decodeURIComponent(projectId);
6807
6704
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables`);
6808
6705
  Object.entries(options).forEach(([key, value]) => {
6809
- if (value === undefined)
6810
- return;
6811
6706
  if (key === "filter" && typeof value === "object" && value !== null) {
6812
- Object.entries(value).forEach(([fKey, fVal]) => {
6813
- url.searchParams.append(`filter[${fKey}]`, fVal);
6707
+ Object.entries(value).forEach(([filterKey, filterValue]) => {
6708
+ appendFilterParam(url.searchParams, `filter[${filterKey}]`, filterValue);
6814
6709
  });
6710
+ return;
6815
6711
  }
6816
- else if (typeof value === "boolean") {
6817
- url.searchParams.append(key, value ? "true" : "false");
6818
- }
6819
- else {
6820
- url.searchParams.append(key, String(value));
6821
- }
6712
+ appendFilterParam(url.searchParams, key, value);
6822
6713
  });
6823
6714
  const response = await fetch(url.toString(), getFetchConfig());
6824
6715
  await handleGitLabError(response);
@@ -6872,19 +6763,13 @@ async function listGroupVariables(groupId, options = {}) {
6872
6763
  const encoded = encodeURIComponent(decodeURIComponent(groupId));
6873
6764
  const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables`);
6874
6765
  Object.entries(options).forEach(([key, value]) => {
6875
- if (value === undefined)
6876
- return;
6877
6766
  if (key === "filter" && typeof value === "object" && value !== null) {
6878
- Object.entries(value).forEach(([fKey, fVal]) => {
6879
- url.searchParams.append(`filter[${fKey}]`, fVal);
6767
+ Object.entries(value).forEach(([filterKey, filterValue]) => {
6768
+ appendFilterParam(url.searchParams, `filter[${filterKey}]`, filterValue);
6880
6769
  });
6770
+ return;
6881
6771
  }
6882
- else if (typeof value === "boolean") {
6883
- url.searchParams.append(key, value ? "true" : "false");
6884
- }
6885
- else {
6886
- url.searchParams.append(key, String(value));
6887
- }
6772
+ appendFilterParam(url.searchParams, key, value);
6888
6773
  });
6889
6774
  const response = await fetch(url.toString(), getFetchConfig());
6890
6775
  await handleGitLabError(response);
@@ -7431,11 +7316,7 @@ async function downloadAttachment(projectId, secret, filename, localPath) {
7431
7316
  async function listEvents(options = {}) {
7432
7317
  const url = new URL(`${getEffectiveApiUrl()}/events`);
7433
7318
  // Add all query parameters
7434
- Object.entries(options).forEach(([key, value]) => {
7435
- if (value !== undefined) {
7436
- url.searchParams.append(key, value.toString());
7437
- }
7438
- });
7319
+ appendFilterParams(url.searchParams, options);
7439
7320
  const response = await fetch(url.toString(), { ...getFetchConfig() });
7440
7321
  if (!response.ok) {
7441
7322
  await handleGitLabError(response);
@@ -7453,11 +7334,7 @@ async function getProjectEvents(projectId, options = {}) {
7453
7334
  const effectiveProjectId = getEffectiveProjectId(projectId);
7454
7335
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/events`);
7455
7336
  // Add all query parameters
7456
- Object.entries(options).forEach(([key, value]) => {
7457
- if (value !== undefined) {
7458
- url.searchParams.append(key, value.toString());
7459
- }
7460
- });
7337
+ appendFilterParams(url.searchParams, options);
7461
7338
  const response = await fetch(url.toString(), { ...getFetchConfig() });
7462
7339
  if (!response.ok) {
7463
7340
  await handleGitLabError(response);
@@ -7476,11 +7353,7 @@ async function listReleases(projectId, options = {}) {
7476
7353
  const effectiveProjectId = getEffectiveProjectId(projectId);
7477
7354
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases`);
7478
7355
  // Add query parameters
7479
- Object.entries(options).forEach(([key, value]) => {
7480
- if (value !== undefined) {
7481
- url.searchParams.append(key, value.toString());
7482
- }
7483
- });
7356
+ appendFilterParams(url.searchParams, options);
7484
7357
  const response = await fetch(url.toString(), {
7485
7358
  ...getFetchConfig(),
7486
7359
  });
@@ -7603,11 +7476,7 @@ async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
7603
7476
  async function listTags(projectId, options = {}) {
7604
7477
  const effectiveProjectId = getEffectiveProjectId(projectId);
7605
7478
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags`);
7606
- Object.entries(options).forEach(([key, value]) => {
7607
- if (value !== undefined) {
7608
- url.searchParams.append(key, value.toString());
7609
- }
7610
- });
7479
+ appendFilterParams(url.searchParams, options);
7611
7480
  const response = await fetch(url.toString(), {
7612
7481
  ...getFetchConfig(),
7613
7482
  });
@@ -8847,8 +8716,8 @@ async function handleToolCall(params) {
8847
8716
  };
8848
8717
  }
8849
8718
  case "list_labels": {
8850
- const args = ListLabelsSchema.parse(params.arguments);
8851
- 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);
8852
8721
  return {
8853
8722
  content: [{ type: "text", text: JSON.stringify(labels) }],
8854
8723
  };
package/build/schemas.js CHANGED
@@ -3075,7 +3075,7 @@ export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({
3075
3075
  iids: z
3076
3076
  .array(z.coerce.number())
3077
3077
  .optional()
3078
- .describe("Return only the milestones having the given iid"),
3078
+ .describe("Return only the milestones having the given iid. An empty array is treated as no filter and is left out of the request."),
3079
3079
  state: z
3080
3080
  .enum(["active", "closed"])
3081
3081
  .optional()
@@ -3137,7 +3137,7 @@ export const ListGroupMilestonesSchema = z
3137
3137
  iids: z
3138
3138
  .array(z.coerce.number())
3139
3139
  .optional()
3140
- .describe("Return only the milestones having the given iid"),
3140
+ .describe("Return only the milestones having the given iid. An empty array is treated as no filter and is left out of the request."),
3141
3141
  state: z
3142
3142
  .enum(["active", "closed"])
3143
3143
  .optional()
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Generate docs/reference/tool-coverage.md from tools/registry.ts + test/ MCP calls.
4
+ *
5
+ * Detection rules live in ./tool-coverage/coverage.ts (AST-scoped): a tool is
6
+ * invoked when test source sends that name as a `callTool*` argument, as the
7
+ * `name` of a first-argument params object (`client.callTool({ name })`), or as
8
+ * the `name` of a `params` object inside a `tools/call` payload literal.
9
+ *
10
+ * Name-only mentions (toolset tables, description-quality) do not count.
11
+ * callTool(variable) loops are missed unless the name is also a literal in that call.
12
+ *
13
+ * Run with: npm run docs:tools
14
+ */
15
+ import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
16
+ import { dirname, join, relative } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { allTools, TOOLSET_DEFINITIONS } from "../tools/registry.js";
19
+ import { assertDetectorHealthy, buildCoverageMarkdown, collectInvocations, countInvoked, groupToolsByToolset, percent, SKIP_FILES, } from "./tool-coverage/coverage.js";
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const REPO_ROOT = join(__dirname, "..");
22
+ const TEST_DIR = join(REPO_ROOT, "test");
23
+ const OUT_PATH = join(REPO_ROOT, "docs", "reference", "tool-coverage.md");
24
+ function walkTestFiles(dir) {
25
+ const out = [];
26
+ for (const name of readdirSync(dir)) {
27
+ const full = join(dir, name);
28
+ const stat = statSync(full);
29
+ if (stat.isDirectory()) {
30
+ out.push(...walkTestFiles(full));
31
+ continue;
32
+ }
33
+ if (/\.(ts|js)$/.test(name) && !SKIP_FILES.has(name)) {
34
+ out.push(full);
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ function repoPath(abs) {
40
+ return relative(REPO_ROOT, abs).split("\\").join("/");
41
+ }
42
+ function readScannedFiles() {
43
+ return walkTestFiles(TEST_DIR).map(abs => ({
44
+ path: repoPath(abs),
45
+ text: readFileSync(abs, "utf8"),
46
+ }));
47
+ }
48
+ function main() {
49
+ const toolNames = allTools.map(tool => tool.name);
50
+ const toolNameSet = new Set(toolNames);
51
+ const hits = collectInvocations(readScannedFiles(), toolNameSet);
52
+ const invokedCount = countInvoked(toolNames, hits);
53
+ assertDetectorHealthy(hits, invokedCount);
54
+ const grouped = groupToolsByToolset(toolNames, TOOLSET_DEFINITIONS);
55
+ writeFileSync(OUT_PATH, buildCoverageMarkdown(toolNames, hits, grouped));
56
+ console.log(`generated docs/reference/tool-coverage.md — ${invokedCount}/${toolNames.length} (${percent(invokedCount, toolNames.length)})`);
57
+ }
58
+ main();