@zereight/mcp-gitlab 2.1.28 → 2.1.29

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
@@ -71,7 +71,7 @@ npm install -g @zereight/mcp-gitlab
71
71
 
72
72
  예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
73
73
 
74
- 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.27`처럼 버전을 고정하세요.
74
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.29`처럼 버전을 고정하세요.
75
75
 
76
76
  #### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
77
77
 
@@ -94,12 +94,19 @@ npm install -g @zereight/mcp-gitlab
94
94
  - `--token` - GitLab Personal Access Token (`GITLAB_PERSONAL_ACCESS_TOKEN` 대체)
95
95
  - `--api-url` - GitLab API URL (`GITLAB_API_URL` 대체)
96
96
  - `--read-only=true` - 읽기 전용 모드 활성화 (`GITLAB_READ_ONLY_MODE` 대체)
97
- - `--use-wiki=true` - 위키 API 활성화 (`USE_GITLAB_WIKI` 대체)
98
- - `--use-milestone=true` - 마일스톤 API 활성화 (`USE_MILESTONE` 대체)
99
- - `--use-pipeline=true` - 파이프라인 API 활성화 (`USE_PIPELINE` 대체)
97
+ - `--use-wiki=true` - 위키 API 활성화 (`USE_GITLAB_WIKI` 대체, 레거시 — `GITLAB_TOOLSETS=wiki` 권장)
98
+ - `--use-milestone=true` - 마일스톤 API 활성화 (`USE_MILESTONE` 대체, 레거시 — `GITLAB_TOOLSETS=milestones` 권장)
99
+ - `--use-pipeline=true` - 파이프라인 API 활성화 (`USE_PIPELINE` 대체, 레거시 — `GITLAB_TOOLSETS=pipelines` 권장)
100
100
 
101
101
  CLI 인자는 환경 변수보다 우선합니다.
102
102
 
103
+ > **세밀한 도구 필터링:** 전체 on/off 방식인 `GITLAB_READ_ONLY_MODE` 외에도,
104
+ > `GITLAB_TOOLSETS=<group,…>`로 도구 그룹을 활성화하고, `GITLAB_TOOLS=<tool,…>`로 개별 도구만
105
+ > 허용하며(예: 읽기 도구 + 특정 쓰기 도구 몇 개), `GITLAB_DENIED_TOOLS_REGEX`로 패턴 차단할 수
106
+ > 있습니다. 레거시 `USE_GITLAB_WIKI` / `USE_MILESTONE` / `USE_PIPELINE` 플래그는 하위 호환용으로만
107
+ > 유지됩니다. [Tools Reference](./docs/tools/index.md#feature-toggles)와
108
+ > [Environment Variables](./docs/configuration/environment-variables.md)를 참고하세요.
109
+
103
110
  #### SSE
104
111
 
105
112
  ```shell
@@ -108,9 +115,7 @@ docker run -i --rm \
108
115
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
109
116
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
110
117
  -e GITLAB_READ_ONLY_MODE=true \
111
- -e USE_GITLAB_WIKI=true \
112
- -e USE_MILESTONE=true \
113
- -e USE_PIPELINE=true \
118
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
114
119
  -e SSE=true \
115
120
  -p 3333:3002 \
116
121
  zereight050/gitlab-mcp
@@ -135,9 +140,7 @@ docker run -i --rm \
135
140
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
136
141
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
137
142
  -e GITLAB_READ_ONLY_MODE=true \
138
- -e USE_GITLAB_WIKI=true \
139
- -e USE_MILESTONE=true \
140
- -e USE_PIPELINE=true \
143
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
141
144
  -e STREAMABLE_HTTP=true \
142
145
  -p 3333:3002 \
143
146
  zereight050/gitlab-mcp
package/README.md CHANGED
@@ -75,7 +75,7 @@ npm install -g @zereight/mcp-gitlab
75
75
 
76
76
  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`.
77
77
 
78
- No global install? Pin `npx` to a known version, for example `npx -y @zereight/mcp-gitlab@2.1.27`.
78
+ No global install? Pin `npx` to a known version, for example `npx -y @zereight/mcp-gitlab@2.1.29`.
79
79
 
80
80
  #### Using CLI Arguments (for clients with env var issues)
81
81
 
@@ -98,12 +98,20 @@ Some MCP clients (like GitHub Copilot CLI) have issues with environment variable
98
98
  - `--token` - GitLab Personal Access Token (replaces `GITLAB_PERSONAL_ACCESS_TOKEN`)
99
99
  - `--api-url` - GitLab API URL (replaces `GITLAB_API_URL`)
100
100
  - `--read-only=true` - Enable read-only mode (replaces `GITLAB_READ_ONLY_MODE`)
101
- - `--use-wiki=true` - Enable wiki API (replaces `USE_GITLAB_WIKI`)
102
- - `--use-milestone=true` - Enable milestone API (replaces `USE_MILESTONE`)
103
- - `--use-pipeline=true` - Enable pipeline API (replaces `USE_PIPELINE`)
101
+ - `--use-wiki=true` - Enable wiki API (replaces `USE_GITLAB_WIKI`, legacy — prefer `GITLAB_TOOLSETS=wiki`)
102
+ - `--use-milestone=true` - Enable milestone API (replaces `USE_MILESTONE`, legacy — prefer `GITLAB_TOOLSETS=milestones`)
103
+ - `--use-pipeline=true` - Enable pipeline API (replaces `USE_PIPELINE`, legacy — prefer `GITLAB_TOOLSETS=pipelines`)
104
104
 
105
105
  CLI arguments take precedence over environment variables.
106
106
 
107
+ > **Fine-grained tool filtering:** beyond the all-or-nothing `GITLAB_READ_ONLY_MODE`, you can
108
+ > enable toolset groups with `GITLAB_TOOLSETS=<group,…>`, allow-list individual tools with
109
+ > `GITLAB_TOOLS=<tool,…>` (e.g. read-only groups plus a few specific write tools), and
110
+ > deny-list by pattern with `GITLAB_DENIED_TOOLS_REGEX`. The legacy `USE_GITLAB_WIKI` /
111
+ > `USE_MILESTONE` / `USE_PIPELINE` flags are kept for backward compatibility only.
112
+ > See [Tools Reference](./docs/tools/index.md#feature-toggles) and
113
+ > [Environment Variables](./docs/configuration/environment-variables.md).
114
+
107
115
  - sse
108
116
 
109
117
  ```shell
@@ -112,9 +120,7 @@ docker run -i --rm \
112
120
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
113
121
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
114
122
  -e GITLAB_READ_ONLY_MODE=true \
115
- -e USE_GITLAB_WIKI=true \
116
- -e USE_MILESTONE=true \
117
- -e USE_PIPELINE=true \
123
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
118
124
  -e SSE=true \
119
125
  -e SSE_AUTH_TOKEN=your_mcp_sse_token \
120
126
  -p 3333:3002 \
@@ -143,9 +149,7 @@ docker run -i --rm \
143
149
  -e REMOTE_AUTHORIZATION=true \
144
150
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
145
151
  -e GITLAB_READ_ONLY_MODE=true \
146
- -e USE_GITLAB_WIKI=true \
147
- -e USE_MILESTONE=true \
148
- -e USE_PIPELINE=true \
152
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
149
153
  -e STREAMABLE_HTTP=true \
150
154
  -p 3333:3002 \
151
155
  zereight050/gitlab-mcp
package/README.zh-CN.md CHANGED
@@ -71,7 +71,7 @@ npm install -g @zereight/mcp-gitlab
71
71
 
72
72
  示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
73
73
 
74
- 如果不想全局安装,请固定 `npx` 版本,例如 `npx -y @zereight/mcp-gitlab@2.1.27`。
74
+ 如果不想全局安装,请固定 `npx` 版本,例如 `npx -y @zereight/mcp-gitlab@2.1.29`。
75
75
 
76
76
  #### 使用 CLI 参数(适用于环境变量有问题的客户端)
77
77
 
@@ -94,12 +94,19 @@ npm install -g @zereight/mcp-gitlab
94
94
  - `--token` - GitLab Personal Access Token(替代 `GITLAB_PERSONAL_ACCESS_TOKEN`)
95
95
  - `--api-url` - GitLab API URL(替代 `GITLAB_API_URL`)
96
96
  - `--read-only=true` - 启用只读模式(替代 `GITLAB_READ_ONLY_MODE`)
97
- - `--use-wiki=true` - 启用 Wiki API(替代 `USE_GITLAB_WIKI`)
98
- - `--use-milestone=true` - 启用里程碑 API(替代 `USE_MILESTONE`)
99
- - `--use-pipeline=true` - 启用流水线 API(替代 `USE_PIPELINE`)
97
+ - `--use-wiki=true` - 启用 Wiki API(替代 `USE_GITLAB_WIKI`,旧版 — 推荐 `GITLAB_TOOLSETS=wiki`)
98
+ - `--use-milestone=true` - 启用里程碑 API(替代 `USE_MILESTONE`,旧版 — 推荐 `GITLAB_TOOLSETS=milestones`)
99
+ - `--use-pipeline=true` - 启用流水线 API(替代 `USE_PIPELINE`,旧版 — 推荐 `GITLAB_TOOLSETS=pipelines`)
100
100
 
101
101
  CLI 参数优先于环境变量。
102
102
 
103
+ > **细粒度工具过滤:**除了全开/全关的 `GITLAB_READ_ONLY_MODE`,还可以用
104
+ > `GITLAB_TOOLSETS=<group,…>` 启用工具分组,用 `GITLAB_TOOLS=<tool,…>` 白名单启用单个工具
105
+ > (例如:只读分组 + 少数几个写工具),用 `GITLAB_DENIED_TOOLS_REGEX` 按正则屏蔽工具。
106
+ > 旧版 `USE_GITLAB_WIKI` / `USE_MILESTONE` / `USE_PIPELINE` 标志仅为向后兼容保留。
107
+ > 参见 [Tools Reference](./docs/tools/index.md#feature-toggles) 和
108
+ > [Environment Variables](./docs/configuration/environment-variables.md)。
109
+
103
110
  #### SSE
104
111
 
105
112
  ```shell
@@ -108,9 +115,7 @@ docker run -i --rm \
108
115
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
109
116
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
110
117
  -e GITLAB_READ_ONLY_MODE=true \
111
- -e USE_GITLAB_WIKI=true \
112
- -e USE_MILESTONE=true \
113
- -e USE_PIPELINE=true \
118
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
114
119
  -e SSE=true \
115
120
  -p 3333:3002 \
116
121
  zereight050/gitlab-mcp
@@ -135,9 +140,7 @@ docker run -i --rm \
135
140
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
136
141
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
137
142
  -e GITLAB_READ_ONLY_MODE=true \
138
- -e USE_GITLAB_WIKI=true \
139
- -e USE_MILESTONE=true \
140
- -e USE_PIPELINE=true \
143
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
141
144
  -e STREAMABLE_HTTP=true \
142
145
  -p 3333:3002 \
143
146
  zereight050/gitlab-mcp
package/build/index.js CHANGED
@@ -134,6 +134,7 @@ import { normalizeGitLabApiUrl } from "./utils/url.js";
134
134
  import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents, } from "./utils/helpers.js";
135
135
  import { graphqlQueryContainsWriteOperation } from "./utils/graphql-query.js";
136
136
  import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
137
+ import { redactSensitiveGitLabFields } from "./utils/redact-sensitive.js";
137
138
  import { cleanMutuallyExclusiveIdUsernameOptions, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
138
139
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
139
140
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
@@ -1028,7 +1029,13 @@ function parseAllowedGitLabApiUrls(value) {
1028
1029
  .filter((entry) => Boolean(entry));
1029
1030
  }
1030
1031
  function encodeGitLabPathSegment(value) {
1031
- return encodeURIComponent(decodeURIComponent(value));
1032
+ const segment = String(value);
1033
+ try {
1034
+ return encodeURIComponent(decodeURIComponent(segment));
1035
+ }
1036
+ catch {
1037
+ return encodeURIComponent(segment);
1038
+ }
1032
1039
  }
1033
1040
  function encodeGitLabPath(value) {
1034
1041
  return value.split("/").map(encodeGitLabPathSegment).join("/");
@@ -1418,7 +1425,7 @@ async function listMergeRequests(projectId, options = {}) {
1418
1425
  */
1419
1426
  async function getIssue(projectId, issueIid) {
1420
1427
  projectId = decodeURIComponent(projectId); // Decode project ID
1421
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`);
1428
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1422
1429
  const response = await fetch(url.toString(), {
1423
1430
  ...getFetchConfig(),
1424
1431
  });
@@ -1437,7 +1444,7 @@ async function getIssue(projectId, issueIid) {
1437
1444
  */
1438
1445
  async function updateIssue(projectId, issueIid, options) {
1439
1446
  projectId = decodeURIComponent(projectId); // Decode project ID
1440
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`);
1447
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1441
1448
  // Convert labels array to comma-separated string if present
1442
1449
  const body = { ...options };
1443
1450
  if (body.labels && Array.isArray(body.labels)) {
@@ -1462,7 +1469,7 @@ async function updateIssue(projectId, issueIid, options) {
1462
1469
  */
1463
1470
  async function deleteIssue(projectId, issueIid) {
1464
1471
  projectId = decodeURIComponent(projectId); // Decode project ID
1465
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`);
1472
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1466
1473
  const response = await fetch(url.toString(), {
1467
1474
  ...getFetchConfig(),
1468
1475
  method: "DELETE",
@@ -2806,7 +2813,7 @@ async function updateWorkItem(projectId, iid, options) {
2806
2813
  */
2807
2814
  async function listIssueLinks(projectId, issueIid) {
2808
2815
  projectId = decodeURIComponent(projectId); // Decode project ID
2809
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links`);
2816
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
2810
2817
  const response = await fetch(url.toString(), {
2811
2818
  ...getFetchConfig(),
2812
2819
  });
@@ -2825,7 +2832,7 @@ async function listIssueLinks(projectId, issueIid) {
2825
2832
  */
2826
2833
  async function getIssueLink(projectId, issueIid, issueLinkId) {
2827
2834
  projectId = decodeURIComponent(projectId); // Decode project ID
2828
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links/${issueLinkId}`);
2835
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
2829
2836
  const response = await fetch(url.toString(), {
2830
2837
  ...getFetchConfig(),
2831
2838
  });
@@ -2847,7 +2854,7 @@ async function getIssueLink(projectId, issueIid, issueLinkId) {
2847
2854
  async function createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = "relates_to") {
2848
2855
  projectId = decodeURIComponent(projectId); // Decode project ID
2849
2856
  targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well
2850
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links`);
2857
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
2851
2858
  const response = await fetch(url.toString(), {
2852
2859
  ...getFetchConfig(),
2853
2860
  method: "POST",
@@ -2872,7 +2879,7 @@ async function createIssueLink(projectId, issueIid, targetProjectId, targetIssue
2872
2879
  */
2873
2880
  async function deleteIssueLink(projectId, issueIid, issueLinkId) {
2874
2881
  projectId = decodeURIComponent(projectId); // Decode project ID
2875
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links/${issueLinkId}`);
2882
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
2876
2883
  const response = await fetch(url.toString(), {
2877
2884
  ...getFetchConfig(),
2878
2885
  method: "DELETE",
@@ -2995,7 +3002,7 @@ async function listIssueDiscussions(projectId, issueIid, options = {}) {
2995
3002
  }
2996
3003
  async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId) {
2997
3004
  projectId = decodeURIComponent(projectId); // Decode project ID
2998
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
3005
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
2999
3006
  const response = await fetch(url.toString(), {
3000
3007
  ...getFetchConfig(),
3001
3008
  method: "DELETE",
@@ -3019,7 +3026,7 @@ async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3019
3026
  */
3020
3027
  async function updateMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
3021
3028
  projectId = decodeURIComponent(projectId); // Decode project ID
3022
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
3029
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3023
3030
  // Only one of body or resolved can be sent according to GitLab API
3024
3031
  const payload = {};
3025
3032
  if (body !== undefined) {
@@ -3065,7 +3072,7 @@ async function updateMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3065
3072
  */
3066
3073
  async function updateIssueNote(projectId, issueIid, discussionId, noteId, body, resolved) {
3067
3074
  projectId = decodeURIComponent(projectId); // Decode project ID
3068
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/discussions/${discussionId}/notes/${noteId}`);
3075
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3069
3076
  // Only one of body or resolved can be sent according to GitLab API
3070
3077
  const payload = {};
3071
3078
  if (body !== undefined) {
@@ -3094,8 +3101,8 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
3094
3101
  */
3095
3102
  async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
3096
3103
  projectId = decodeURIComponent(projectId); // Decode project ID
3097
- const basePath = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`;
3098
- const url = new URL(discussionId ? `${basePath}/discussions/${discussionId}/notes` : `${basePath}/notes`);
3104
+ const basePath = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
3105
+ const url = new URL(discussionId ? `${basePath}/discussions/${encodeGitLabPathSegment(discussionId)}/notes` : `${basePath}/notes`);
3099
3106
  const payload = { body };
3100
3107
  if (createdAt) {
3101
3108
  payload.created_at = createdAt;
@@ -3122,7 +3129,7 @@ async function createIssueNote(projectId, issueIid, discussionId, body, createdA
3122
3129
  */
3123
3130
  async function createMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, body, createdAt) {
3124
3131
  projectId = decodeURIComponent(projectId); // Decode project ID
3125
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes`);
3132
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`);
3126
3133
  const payload = { body };
3127
3134
  if (createdAt) {
3128
3135
  payload.created_at = createdAt;
@@ -3138,7 +3145,7 @@ async function createMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3138
3145
  }
3139
3146
  async function createMergeRequestNote(projectId, mergeRequestIid, body) {
3140
3147
  projectId = decodeURIComponent(projectId); // Decode project ID
3141
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes`);
3148
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3142
3149
  const payload = {
3143
3150
  id: projectId,
3144
3151
  merge_request_iid: mergeRequestIid,
@@ -3155,7 +3162,7 @@ async function createMergeRequestNote(projectId, mergeRequestIid, body) {
3155
3162
  }
3156
3163
  async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
3157
3164
  projectId = decodeURIComponent(projectId); // Decode project ID
3158
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
3165
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3159
3166
  const response = await fetch(url.toString(), {
3160
3167
  ...getFetchConfig(),
3161
3168
  method: "DELETE",
@@ -3203,7 +3210,7 @@ async function deleteRestAwardEmoji(path) {
3203
3210
  }
3204
3211
  async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
3205
3212
  projectId = decodeURIComponent(projectId); // Decode project ID
3206
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
3213
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3207
3214
  const response = await fetch(url.toString(), {
3208
3215
  ...getFetchConfig(),
3209
3216
  method: "GET",
@@ -3214,7 +3221,7 @@ async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
3214
3221
  }
3215
3222
  async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by, per_page, page) {
3216
3223
  projectId = decodeURIComponent(projectId); // Decode project ID
3217
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes`);
3224
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3218
3225
  if (sort) {
3219
3226
  url.searchParams.append("sort", sort);
3220
3227
  }
@@ -3237,7 +3244,7 @@ async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by,
3237
3244
  }
3238
3245
  async function updateMergeRequestNote(projectId, mergeRequestIid, noteId, body) {
3239
3246
  projectId = decodeURIComponent(projectId); // Decode project ID
3240
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
3247
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3241
3248
  const payload = {
3242
3249
  id: projectId,
3243
3250
  merge_request_iid: mergeRequestIid,
@@ -3415,8 +3422,7 @@ async function searchBlobs(params) {
3415
3422
  basePath = `${getEffectiveApiUrl()}/projects/${projectId}/search`;
3416
3423
  }
3417
3424
  else if (params.group_id) {
3418
- const groupId = encodeURIComponent(decodeURIComponent(params.group_id));
3419
- basePath = `${getEffectiveApiUrl()}/groups/${groupId}/search`;
3425
+ basePath = `${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(params.group_id)}/search`;
3420
3426
  }
3421
3427
  else {
3422
3428
  basePath = `${getEffectiveApiUrl()}/search`;
@@ -3490,7 +3496,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
3490
3496
  projectId = decodeURIComponent(projectId); // Decode project ID
3491
3497
  let url;
3492
3498
  if (mergeRequestIid) {
3493
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
3499
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
3494
3500
  url.searchParams.append("include_diverged_commits_count", "true");
3495
3501
  }
3496
3502
  else if (branchName) {
@@ -3512,7 +3518,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
3512
3518
  return GitLabMergeRequestSchema.parse(data);
3513
3519
  }
3514
3520
  async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3515
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/commits`);
3521
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/commits`);
3516
3522
  url.searchParams.append("per_page", "100");
3517
3523
  let totalCount = 0;
3518
3524
  let page = 1;
@@ -3540,7 +3546,7 @@ async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3540
3546
  }
3541
3547
  async function listMergeRequestPipelines(projectId, mergeRequestIid, options = {}) {
3542
3548
  projectId = decodeURIComponent(projectId);
3543
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/pipelines`);
3549
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/pipelines`);
3544
3550
  Object.entries(options).forEach(([key, value]) => {
3545
3551
  if (value !== undefined) {
3546
3552
  url.searchParams.append(key, value.toString());
@@ -3741,7 +3747,7 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view
3741
3747
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
3742
3748
  mergeRequestIid = mergeRequest.iid;
3743
3749
  }
3744
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/changes`);
3750
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
3745
3751
  if (view) {
3746
3752
  url.searchParams.append("view", view);
3747
3753
  }
@@ -3771,7 +3777,7 @@ async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, pag
3771
3777
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
3772
3778
  mergeRequestIid = mergeRequest.iid;
3773
3779
  }
3774
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/diffs`);
3780
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
3775
3781
  if (page) {
3776
3782
  url.searchParams.append("page", page.toString());
3777
3783
  }
@@ -3807,7 +3813,7 @@ async function listMergeRequestChangedFiles(projectId, mergeRequestIid, branchNa
3807
3813
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
3808
3814
  mergeRequestIid = mergeRequest.iid;
3809
3815
  }
3810
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/changes`);
3816
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
3811
3817
  const response = await fetch(url.toString(), { ...getFetchConfig() });
3812
3818
  await handleGitLabError(response);
3813
3819
  const data = (await response.json());
@@ -3848,7 +3854,7 @@ async function getMergeRequestFileDiff(projectId, filePaths, mergeRequestIid, br
3848
3854
  let page = 1;
3849
3855
  const perPage = 20;
3850
3856
  while (remaining.size > 0) {
3851
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/diffs`);
3857
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
3852
3858
  url.searchParams.append("page", page.toString());
3853
3859
  url.searchParams.append("per_page", perPage.toString());
3854
3860
  if (unidiff) {
@@ -3926,7 +3932,7 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
3926
3932
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
3927
3933
  mergeRequestIid = mergeRequest.iid;
3928
3934
  }
3929
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
3935
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
3930
3936
  const response = await fetch(url.toString(), {
3931
3937
  ...getFetchConfig(),
3932
3938
  method: "PUT",
@@ -3946,7 +3952,7 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
3946
3952
  */
3947
3953
  async function mergeMergeRequest(projectId, options, mergeRequestIid) {
3948
3954
  projectId = decodeURIComponent(projectId); // Decode project ID
3949
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/merge`);
3955
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/merge`);
3950
3956
  const response = await fetch(url.toString(), {
3951
3957
  ...getFetchConfig(),
3952
3958
  method: "PUT",
@@ -3966,7 +3972,7 @@ async function mergeMergeRequest(projectId, options, mergeRequestIid) {
3966
3972
  */
3967
3973
  async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPassword) {
3968
3974
  projectId = decodeURIComponent(projectId);
3969
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approve`);
3975
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approve`);
3970
3976
  const body = {};
3971
3977
  if (sha) {
3972
3978
  body.sha = sha;
@@ -3991,7 +3997,7 @@ async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPass
3991
3997
  */
3992
3998
  async function unapproveMergeRequest(projectId, mergeRequestIid) {
3993
3999
  projectId = decodeURIComponent(projectId);
3994
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/unapprove`);
4000
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/unapprove`);
3995
4001
  const response = await fetch(url.toString(), {
3996
4002
  ...getFetchConfig(),
3997
4003
  method: "POST",
@@ -4009,7 +4015,7 @@ async function unapproveMergeRequest(projectId, mergeRequestIid) {
4009
4015
  */
4010
4016
  async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
4011
4017
  projectId = decodeURIComponent(projectId);
4012
- const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approval_state`);
4018
+ const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approval_state`);
4013
4019
  const approvalStateResponse = await fetch(approvalStateUrl.toString(), {
4014
4020
  ...getFetchConfig(),
4015
4021
  method: "GET",
@@ -4037,7 +4043,7 @@ async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
4037
4043
  */
4038
4044
  async function getMergeRequestConflicts(projectId, mergeRequestIid) {
4039
4045
  projectId = decodeURIComponent(projectId);
4040
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/conflicts`);
4046
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/conflicts`);
4041
4047
  const response = await fetch(url.toString(), {
4042
4048
  ...getFetchConfig(),
4043
4049
  method: "GET",
@@ -4046,7 +4052,7 @@ async function getMergeRequestConflicts(projectId, mergeRequestIid) {
4046
4052
  return (await response.json());
4047
4053
  }
4048
4054
  async function getMergeRequestApprovalsFallback(projectId, mergeRequestIid) {
4049
- const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approvals`);
4055
+ const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approvals`);
4050
4056
  const approvalsResponse = await fetch(approvalsUrl.toString(), {
4051
4057
  ...getFetchConfig(),
4052
4058
  method: "GET",
@@ -4136,7 +4142,7 @@ async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
4136
4142
  }
4137
4143
  async function listDraftNotes(projectId, mergeRequestIid) {
4138
4144
  projectId = decodeURIComponent(projectId);
4139
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
4145
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4140
4146
  const response = await fetch(url.toString(), {
4141
4147
  ...getFetchConfig(),
4142
4148
  method: "GET",
@@ -4160,7 +4166,7 @@ async function listDraftNotes(projectId, mergeRequestIid) {
4160
4166
  */
4161
4167
  async function createDraftNote(projectId, mergeRequestIid, body, inReplyToDiscussionId, position, resolveDiscussion) {
4162
4168
  projectId = decodeURIComponent(projectId);
4163
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
4169
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4164
4170
  const requestBody = { note: body };
4165
4171
  if (inReplyToDiscussionId) {
4166
4172
  requestBody.in_reply_to_discussion_id = inReplyToDiscussionId;
@@ -4195,7 +4201,7 @@ async function createDraftNote(projectId, mergeRequestIid, body, inReplyToDiscus
4195
4201
  */
4196
4202
  async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, position, resolveDiscussion) {
4197
4203
  projectId = decodeURIComponent(projectId);
4198
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
4204
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4199
4205
  const requestBody = {};
4200
4206
  if (body !== undefined) {
4201
4207
  requestBody.note = body;
@@ -4227,7 +4233,7 @@ async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, po
4227
4233
  */
4228
4234
  async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
4229
4235
  projectId = decodeURIComponent(projectId);
4230
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
4236
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4231
4237
  const response = await fetch(url.toString(), {
4232
4238
  ...getFetchConfig(),
4233
4239
  method: "DELETE",
@@ -4246,7 +4252,7 @@ async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
4246
4252
  */
4247
4253
  async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4248
4254
  projectId = decodeURIComponent(projectId);
4249
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}/publish`);
4255
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}/publish`);
4250
4256
  const response = await fetch(url.toString(), {
4251
4257
  ...getFetchConfig(),
4252
4258
  method: "PUT",
@@ -4298,7 +4304,7 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4298
4304
  */
4299
4305
  async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
4300
4306
  projectId = decodeURIComponent(projectId);
4301
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/bulk_publish`);
4307
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/bulk_publish`);
4302
4308
  const response = await fetch(url.toString(), {
4303
4309
  ...getFetchConfig(),
4304
4310
  method: "POST", // Changed from PUT to POST
@@ -4327,7 +4333,7 @@ async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
4327
4333
  }
4328
4334
  async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionId, resolved) {
4329
4335
  projectId = decodeURIComponent(projectId);
4330
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}`);
4336
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
4331
4337
  if (resolved !== undefined) {
4332
4338
  url.searchParams.append("resolved", resolved ? "true" : "false");
4333
4339
  }
@@ -4359,7 +4365,7 @@ async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionI
4359
4365
  */
4360
4366
  async function createMergeRequestThread(projectId, mergeRequestIid, body, position, createdAt) {
4361
4367
  projectId = decodeURIComponent(projectId); // Decode project ID
4362
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions`);
4368
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions`);
4363
4369
  const payload = { body };
4364
4370
  if (position) {
4365
4371
  payload.position = position;
@@ -4386,7 +4392,7 @@ async function createMergeRequestThread(projectId, mergeRequestIid, body, positi
4386
4392
  */
4387
4393
  async function listMergeRequestVersions(projectId, mergeRequestIid) {
4388
4394
  projectId = decodeURIComponent(projectId); // Decode project ID
4389
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/versions`);
4395
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions`);
4390
4396
  const response = await fetch(url.toString(), {
4391
4397
  ...getFetchConfig(),
4392
4398
  });
@@ -4405,7 +4411,7 @@ async function listMergeRequestVersions(projectId, mergeRequestIid) {
4405
4411
  */
4406
4412
  async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, unidiff) {
4407
4413
  projectId = decodeURIComponent(projectId); // Decode project ID
4408
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/versions/${versionId}`);
4414
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions/${encodeGitLabPathSegment(versionId)}`);
4409
4415
  if (unidiff !== undefined) {
4410
4416
  url.searchParams.append("unidiff", String(unidiff));
4411
4417
  }
@@ -4696,6 +4702,8 @@ async function listWikiPages(projectId, options = {}) {
4696
4702
  url.searchParams.append("per_page", options.per_page.toString());
4697
4703
  if (options.with_content)
4698
4704
  url.searchParams.append("with_content", options.with_content.toString());
4705
+ if (options.render_html)
4706
+ url.searchParams.append("render_html", options.render_html.toString());
4699
4707
  const response = await fetch(url.toString(), {
4700
4708
  ...getFetchConfig(),
4701
4709
  });
@@ -4706,9 +4714,12 @@ async function listWikiPages(projectId, options = {}) {
4706
4714
  /**
4707
4715
  * Get a specific wiki page
4708
4716
  */
4709
- async function getWikiPage(projectId, slug) {
4717
+ async function getWikiPage(projectId, slug, renderHtml) {
4710
4718
  projectId = decodeURIComponent(projectId); // Decode project ID
4711
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, { ...getFetchConfig() });
4719
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`);
4720
+ if (renderHtml)
4721
+ url.searchParams.append("render_html", renderHtml.toString());
4722
+ const response = await fetch(url.toString(), { ...getFetchConfig() });
4712
4723
  await handleGitLabError(response);
4713
4724
  const data = await response.json();
4714
4725
  return GitLabWikiPageSchema.parse(data);
@@ -4781,6 +4792,8 @@ async function listGroupWikiPages(groupId, options = {}) {
4781
4792
  url.searchParams.append("per_page", options.per_page.toString());
4782
4793
  if (options.with_content)
4783
4794
  url.searchParams.append("with_content", options.with_content.toString());
4795
+ if (options.render_html)
4796
+ url.searchParams.append("render_html", options.render_html.toString());
4784
4797
  const response = await fetch(url.toString(), {
4785
4798
  ...getFetchConfig(),
4786
4799
  });
@@ -4791,9 +4804,12 @@ async function listGroupWikiPages(groupId, options = {}) {
4791
4804
  /**
4792
4805
  * Get a specific group wiki page
4793
4806
  */
4794
- async function getGroupWikiPage(groupId, slug) {
4807
+ async function getGroupWikiPage(groupId, slug, renderHtml) {
4795
4808
  groupId = decodeURIComponent(groupId); // Decode group ID
4796
- const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`, { ...getFetchConfig() });
4809
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`);
4810
+ if (renderHtml)
4811
+ url.searchParams.append("render_html", renderHtml.toString());
4812
+ const response = await fetch(url.toString(), { ...getFetchConfig() });
4797
4813
  await handleGitLabError(response);
4798
4814
  const data = await response.json();
4799
4815
  return GitLabWikiPageSchema.parse(data);
@@ -7168,9 +7184,10 @@ async function handleToolCall(params) {
7168
7184
  });
7169
7185
  await handleGitLabError(response);
7170
7186
  const data = await response.json();
7171
- // Return raw data without parsing through our schema to avoid type mismatches in tests
7187
+ // Return raw data without parsing through our schema to avoid type mismatches in tests,
7188
+ // but strip credential fields (e.g. runners_token) so they never reach the AI context.
7172
7189
  return {
7173
- content: [{ type: "text", text: JSON.stringify(data) }],
7190
+ content: [{ type: "text", text: JSON.stringify(redactSensitiveGitLabFields(data)) }],
7174
7191
  };
7175
7192
  }
7176
7193
  case "list_projects": {
@@ -7192,7 +7209,9 @@ async function handleToolCall(params) {
7192
7209
  await handleGitLabError(response);
7193
7210
  const data = await response.json();
7194
7211
  return {
7195
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
7212
+ content: [
7213
+ { type: "text", text: JSON.stringify(redactSensitiveGitLabFields(data), null, 2) },
7214
+ ],
7196
7215
  };
7197
7216
  }
7198
7217
  case "list_project_members": {
@@ -7647,19 +7666,20 @@ async function handleToolCall(params) {
7647
7666
  };
7648
7667
  }
7649
7668
  case "list_wiki_pages": {
7650
- const { project_id, page, per_page, with_content } = ListWikiPagesSchema.parse(params.arguments);
7669
+ const { project_id, page, per_page, with_content, render_html } = ListWikiPagesSchema.parse(params.arguments);
7651
7670
  const wikiPages = await listWikiPages(project_id, {
7652
7671
  page,
7653
7672
  per_page,
7654
7673
  with_content,
7674
+ render_html,
7655
7675
  });
7656
7676
  return {
7657
7677
  content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7658
7678
  };
7659
7679
  }
7660
7680
  case "get_wiki_page": {
7661
- const { project_id, slug } = GetWikiPageSchema.parse(params.arguments);
7662
- const wikiPage = await getWikiPage(project_id, slug);
7681
+ const { project_id, slug, render_html } = GetWikiPageSchema.parse(params.arguments);
7682
+ const wikiPage = await getWikiPage(project_id, slug, render_html);
7663
7683
  return {
7664
7684
  content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7665
7685
  };
@@ -7694,19 +7714,20 @@ async function handleToolCall(params) {
7694
7714
  };
7695
7715
  }
7696
7716
  case "list_group_wiki_pages": {
7697
- const { group_id, page, per_page, with_content } = ListGroupWikiPagesSchema.parse(params.arguments);
7717
+ const { group_id, page, per_page, with_content, render_html } = ListGroupWikiPagesSchema.parse(params.arguments);
7698
7718
  const wikiPages = await listGroupWikiPages(group_id, {
7699
7719
  page,
7700
7720
  per_page,
7701
7721
  with_content,
7722
+ render_html,
7702
7723
  });
7703
7724
  return {
7704
7725
  content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7705
7726
  };
7706
7727
  }
7707
7728
  case "get_group_wiki_page": {
7708
- const { group_id, slug } = GetGroupWikiPageSchema.parse(params.arguments);
7709
- const wikiPage = await getGroupWikiPage(group_id, slug);
7729
+ const { group_id, slug, render_html } = GetGroupWikiPageSchema.parse(params.arguments);
7730
+ const wikiPage = await getGroupWikiPage(group_id, slug, render_html);
7710
7731
  return {
7711
7732
  content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7712
7733
  };
@@ -67,6 +67,12 @@ const REQUIRED_GITLAB_SCOPES_RO = ["read_api"];
67
67
  const PENDING_AUTH_MAX_SIZE = 1000;
68
68
  const PENDING_AUTH_TTL_MS = 10 * 60 * 1000; // 10 minutes
69
69
  const CLIENT_CACHE_MAX_SIZE = 1000;
70
+ function singleQueryParam(value) {
71
+ return typeof value === "string" ? value : undefined;
72
+ }
73
+ function invalidQueryParam(value) {
74
+ return value != null && typeof value !== "string";
75
+ }
70
76
  class BoundedLRUMap {
71
77
  _map = new Map();
72
78
  _maxSize;
@@ -503,11 +509,19 @@ class GitLabOAuthServerProvider {
503
509
  res.status(404).send("Callback proxy mode is not enabled");
504
510
  return;
505
511
  }
506
- const code = req.query.code;
507
- const state = req.query.state;
508
- const error = req.query.error;
512
+ if (invalidQueryParam(req.query.code) ||
513
+ invalidQueryParam(req.query.state) ||
514
+ invalidQueryParam(req.query.error) ||
515
+ invalidQueryParam(req.query.error_description)) {
516
+ res.status(400).send("Invalid query parameter");
517
+ return;
518
+ }
519
+ const code = singleQueryParam(req.query.code);
520
+ const state = singleQueryParam(req.query.state);
521
+ const error = singleQueryParam(req.query.error);
522
+ const errorDescription = singleQueryParam(req.query.error_description) ?? "(no description)";
509
523
  if (error) {
510
- logger.error(`GitLab OAuth error: ${error} — ${req.query.error_description ?? "(no description)"}`);
524
+ logger.error(`GitLab OAuth error: ${error} — ${errorDescription}`);
511
525
  res.status(400).send("Authorization failed");
512
526
  return;
513
527
  }
package/build/schemas.js CHANGED
@@ -2321,11 +2321,19 @@ export const ListWikiPagesSchema = z
2321
2321
  .object({
2322
2322
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
2323
2323
  with_content: z.coerce.boolean().optional().describe("Include content of the wiki pages"),
2324
+ render_html: z.coerce
2325
+ .boolean()
2326
+ .optional()
2327
+ .describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
2324
2328
  })
2325
2329
  .merge(PaginationOptionsSchema);
2326
2330
  export const GetWikiPageSchema = z.object({
2327
2331
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
2328
2332
  slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
2333
+ render_html: z.coerce
2334
+ .boolean()
2335
+ .optional()
2336
+ .describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
2329
2337
  });
2330
2338
  export const CreateWikiPageSchema = z.object({
2331
2339
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
@@ -2350,6 +2358,7 @@ export const GitLabWikiPageSchema = z.object({
2350
2358
  slug: z.string(),
2351
2359
  format: z.string(),
2352
2360
  content: z.string().optional(),
2361
+ front_matter: z.record(z.unknown()).optional(),
2353
2362
  created_at: z.string().optional(),
2354
2363
  updated_at: z.string().optional(),
2355
2364
  });
@@ -2358,11 +2367,19 @@ export const ListGroupWikiPagesSchema = z
2358
2367
  .object({
2359
2368
  group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
2360
2369
  with_content: z.coerce.boolean().optional().describe("Include content of the wiki pages"),
2370
+ render_html: z.coerce
2371
+ .boolean()
2372
+ .optional()
2373
+ .describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
2361
2374
  })
2362
2375
  .merge(PaginationOptionsSchema);
2363
2376
  export const GetGroupWikiPageSchema = z.object({
2364
2377
  group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
2365
2378
  slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
2379
+ render_html: z.coerce
2380
+ .boolean()
2381
+ .optional()
2382
+ .describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
2366
2383
  });
2367
2384
  export const CreateGroupWikiPageSchema = z.object({
2368
2385
  group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { test } from "node:test";
4
+ const source = readFileSync(new URL("../index.ts", import.meta.url), "utf8");
5
+ const rawPathSegmentPatterns = [
6
+ "/issues/${issueIid}",
7
+ "/merge_requests/${mergeRequestIid}",
8
+ "/discussions/${discussionId}",
9
+ "/notes/${noteId}",
10
+ "/links/${issueLinkId}",
11
+ "/draft_notes/${draftNoteId}",
12
+ "/versions/${versionId}",
13
+ "/groups/${groupId}",
14
+ ];
15
+ test("GitLab URL path IDs are encoded before interpolation", () => {
16
+ for (const pattern of rawPathSegmentPatterns) {
17
+ assert.equal(source.includes(pattern), false, `${pattern} must use encodeGitLabPathSegment`);
18
+ }
19
+ });
20
+ test("encoding keeps path traversal payloads inside one URL segment", () => {
21
+ const payload = "1/../../../../admin/ci/variables";
22
+ const url = new URL(`https://gitlab.com/api/v4/projects/42/issues/${encodeURIComponent(payload)}/notes`);
23
+ assert.equal(url.pathname, "/api/v4/projects/42/issues/1%2F..%2F..%2F..%2F..%2Fadmin%2Fci%2Fvariables/notes");
24
+ });
25
+ test("malformed percent-encoded segments can still be encoded", () => {
26
+ assert.equal(encodeURIComponent("bad%zz/path"), "bad%25zz%2Fpath");
27
+ });
@@ -256,6 +256,14 @@ describe("callback-proxy cross-pod flow (stateless)", () => {
256
256
  const tokens = await podC.exchangeAuthorizationCode(podCClient, proxyCode, clientCodeVerifier, CLIENT_REDIRECT);
257
257
  assert.equal(tokens.access_token, "gl-access-123");
258
258
  });
259
+ test("callback rejects repeated query parameters", async () => {
260
+ const pod = makeProvider(loadMaterial(secret()));
261
+ const cbRes = makeFakeRes();
262
+ await pod.handleCallback(fakeReq({ code: "gitlab-code", state: ["one", "two"] }), cbRes);
263
+ assert.equal(cbRes.statusCode, 400);
264
+ assert.equal(cbRes.body, "Invalid query parameter");
265
+ assert.equal(stub.calls.length, 0);
266
+ });
259
267
  test("exchangeAuthorizationCode rejects wrong PKCE verifier", async () => {
260
268
  const s = secret();
261
269
  const pod = makeProvider(loadMaterial(s));
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, test } from "node:test";
3
+ import { redactSensitiveGitLabFields } from "../../utils/redact-sensitive.js";
4
+ describe("When redactSensitiveGitLabFields runs", () => {
5
+ describe("with a project response containing credentials", () => {
6
+ test("should remove runners_token and import_url while keeping other fields", () => {
7
+ const data = {
8
+ id: 42,
9
+ name: "demo",
10
+ runners_token: "GR1348941secret",
11
+ import_url: "https://user:pass@example.com/repo.git",
12
+ };
13
+ const result = redactSensitiveGitLabFields(data);
14
+ assert.equal("runners_token" in result, false);
15
+ assert.equal("import_url" in result, false);
16
+ assert.equal(result.id, 42);
17
+ assert.equal(result.name, "demo");
18
+ });
19
+ });
20
+ describe("with a response that has no sensitive fields", () => {
21
+ test("should return the object unchanged", () => {
22
+ const data = { id: 1, name: "safe" };
23
+ assert.deepEqual(redactSensitiveGitLabFields(data), { id: 1, name: "safe" });
24
+ });
25
+ });
26
+ describe("with a non-object value", () => {
27
+ test("should return the value as-is", () => {
28
+ assert.equal(redactSensitiveGitLabFields(null), null);
29
+ assert.equal(redactSensitiveGitLabFields("raw"), "raw");
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Fields GitLab returns that are live credentials or carry embedded secrets.
3
+ * MCP tool results become part of the AI conversation context, so these must
4
+ * never be forwarded to the model.
5
+ */
6
+ const SENSITIVE_GITLAB_FIELDS = ["runners_token", "import_url"];
7
+ /**
8
+ * Strip known sensitive fields from a GitLab API response object before it is
9
+ * returned as an MCP tool result. Mutates and returns the same object.
10
+ *
11
+ * Used for raw passthrough responses (get_project / update_project) that do not
12
+ * go through a Zod schema, since Zod would otherwise strip unknown keys for us.
13
+ */
14
+ export function redactSensitiveGitLabFields(data) {
15
+ if (data && typeof data === "object") {
16
+ for (const key of SENSITIVE_GITLAB_FIELDS) {
17
+ delete data[key];
18
+ }
19
+ }
20
+ return data;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.1.28",
3
+ "version": "2.1.29",
4
4
  "mcpName": "io.github.zereight/gitlab-mcp",
5
5
  "description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
6
6
  "keywords": [
@@ -52,7 +52,7 @@
52
52
  "changelog": "auto-changelog -p",
53
53
  "test": "npm run test:all",
54
54
  "test:all": "npm run build && npm run test:mock && npm run test:live",
55
- "test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/dynamic-api-url-allowlist.test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/test-oauth-proxy-rate-limit.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && node --import tsx/esm --test test/sse-auth-guard.test.ts && node --import tsx/esm --test test/streamable-http-concurrent-session.test.ts && node --import tsx/esm --test test/streamable-http-unauthenticated-discovery.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-issues.ts && node --import tsx/esm --test test/test-create-repository.ts && node --import tsx/esm --test test/test-update-project.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-remote-downloads.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-protected-branches.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-ci-catalog.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/proxy-client-ip.test.ts && node --import tsx/esm --test test/utils/forwarded-public-base-url.test.ts && node --import tsx/esm --test test/utils/graphql-query.test.ts && node --import tsx/esm --test test/utils/wiki-title.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts && node --import tsx/esm --test test/test-ci-variables.ts && node --import tsx/esm --test test/test-dependency-proxy.ts",
55
+ "test:mock": "node --import tsx/esm --test test/path-segment-encoding.test.ts && node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/dynamic-api-url-allowlist.test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/test-oauth-proxy-rate-limit.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && node --import tsx/esm --test test/sse-auth-guard.test.ts && node --import tsx/esm --test test/streamable-http-concurrent-session.test.ts && node --import tsx/esm --test test/streamable-http-unauthenticated-discovery.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-issues.ts && node --import tsx/esm --test test/test-create-repository.ts && node --import tsx/esm --test test/test-update-project.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-remote-downloads.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-protected-branches.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-ci-catalog.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/proxy-client-ip.test.ts && node --import tsx/esm --test test/utils/forwarded-public-base-url.test.ts && node --import tsx/esm --test test/utils/graphql-query.test.ts && node --import tsx/esm --test test/utils/wiki-title.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/utils/redact-sensitive.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts && node --import tsx/esm --test test/test-ci-variables.ts && node --import tsx/esm --test test/test-dependency-proxy.ts",
56
56
  "test:stateless": "npm run build && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
57
57
  "test:mcp-oauth": "npm run build && node --import tsx/esm --test test/mcp-oauth-tests.ts",
58
58
  "test:live": "node test/validate-api.js",