@zereight/mcp-gitlab 2.3.1 → 2.3.3

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
@@ -138,7 +138,13 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
138
138
 
139
139
  예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
140
140
 
141
- 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.3.0`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
141
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.3.2`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
142
+
143
+ #### Claude Desktop 원클릭 설치(MCP Bundle)
144
+
145
+ v2.3.1 이후의 GitHub 릴리스마다 `gitlab-mcp.mcpb` [MCP Bundle](https://github.com/modelcontextprotocol/mcpb)이 첨부됩니다. [최신 번들](https://github.com/zereight/gitlab-mcp/releases/latest/download/gitlab-mcp.mcpb)을 내려받아 Claude Desktop으로 열거나 **Settings → Extensions**에 끌어다 놓으세요. Claude Desktop이 Personal Access Token을 입력받아 민감 정보로 저장하고, `GITLAB_API_URL`, `GITLAB_PERMISSION_MODE`, `GITLAB_TOOLSETS`, `GITLAB_MASKING_ENABLED`를 설정할 수 있게 합니다. Node.js나 npm을 따로 설치할 필요가 없습니다.
146
+
147
+ 소스에서 번들을 만들려면 `npm run pack:mcpb`를 실행하세요(`gitlab-mcp.mcpb` 생성).
142
148
 
143
149
  #### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
144
150
 
package/README.md CHANGED
@@ -143,7 +143,13 @@ The store path is pinned by your lock file; update it with `nix flake update git
143
143
 
144
144
  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`.
145
145
 
146
- No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.3.0`. 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`).
146
+ No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.3.2`. 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`).
147
+
148
+ #### Claude Desktop one-click install (MCP Bundle)
149
+
150
+ Every GitHub release after v2.3.1 ships a `gitlab-mcp.mcpb` [MCP Bundle](https://github.com/modelcontextprotocol/mcpb). Download the [latest bundle](https://github.com/zereight/gitlab-mcp/releases/latest/download/gitlab-mcp.mcpb) and open it with Claude Desktop (or drag it into **Settings → Extensions**). Claude Desktop asks for your Personal Access Token (stored as a sensitive value) and lets you set `GITLAB_API_URL`, `GITLAB_PERMISSION_MODE`, `GITLAB_TOOLSETS`, and `GITLAB_MASKING_ENABLED`. No separate Node.js or npm install is needed.
151
+
152
+ To build the bundle from source, run `npm run pack:mcpb` (writes `gitlab-mcp.mcpb`).
147
153
 
148
154
  #### Using CLI Arguments (for clients with env var issues)
149
155
 
package/README.pt-BR.md CHANGED
@@ -138,7 +138,13 @@ O caminho no store é fixado pelo seu lock file; atualize-o com `nix flake updat
138
138
 
139
139
  Os exemplos usam `zereight-mcp-gitlab`, um alias com menor chance de conflito que o binário legado `mcp-gitlab`. Se o seu cliente MCP não o encontrar, use o caminho absoluto retornado por `which zereight-mcp-gitlab`.
140
140
 
141
- Não quer instalar globalmente? Fixe o `npx` na versão estável anterior (a versão recomendada por esta documentação), por exemplo `npx -y @zereight/mcp-gitlab@2.3.0`. Se quiser sempre a versão mais recente, use `npx -y @zereight/mcp-gitlab@latest`. O servidor exibe um aviso no stderr durante a inicialização quando existe uma versão mais nova disponível (desative com `GITLAB_DISABLE_VERSION_CHECK=true`).
141
+ Não quer instalar globalmente? Fixe o `npx` na versão estável anterior (a versão recomendada por esta documentação), por exemplo `npx -y @zereight/mcp-gitlab@2.3.2`. Se quiser sempre a versão mais recente, use `npx -y @zereight/mcp-gitlab@latest`. O servidor exibe um aviso no stderr durante a inicialização quando existe uma versão mais nova disponível (desative com `GITLAB_DISABLE_VERSION_CHECK=true`).
142
+
143
+ #### Instalação com um clique no Claude Desktop (MCP Bundle)
144
+
145
+ Cada release no GitHub posterior à v2.3.1 inclui um [MCP Bundle](https://github.com/modelcontextprotocol/mcpb) `gitlab-mcp.mcpb`. Baixe o [bundle mais recente](https://github.com/zereight/gitlab-mcp/releases/latest/download/gitlab-mcp.mcpb) e abra-o com o Claude Desktop (ou arraste-o para **Settings → Extensions**). O Claude Desktop pede seu Personal Access Token (armazenado como valor sensível) e permite definir `GITLAB_API_URL`, `GITLAB_PERMISSION_MODE`, `GITLAB_TOOLSETS` e `GITLAB_MASKING_ENABLED`. Não é preciso instalar Node.js ou npm separadamente.
146
+
147
+ Para gerar o bundle a partir do código-fonte, execute `npm run pack:mcpb` (gera `gitlab-mcp.mcpb`).
142
148
 
143
149
  #### Usando argumentos de CLI (para clientes com problemas com variáveis de ambiente)
144
150
 
package/README.zh-CN.md CHANGED
@@ -138,7 +138,13 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
138
138
 
139
139
  示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
140
140
 
141
- 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.3.0`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
141
+ 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.3.2`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
142
+
143
+ #### Claude Desktop 一键安装(MCP Bundle)
144
+
145
+ v2.3.1 之后的每个 GitHub Release 都附带 `gitlab-mcp.mcpb` [MCP Bundle](https://github.com/modelcontextprotocol/mcpb)。下载[最新的 bundle](https://github.com/zereight/gitlab-mcp/releases/latest/download/gitlab-mcp.mcpb),用 Claude Desktop 打开(或拖到 **Settings → Extensions** 中)。Claude Desktop 会要求输入 Personal Access Token(作为敏感值保存),并可设置 `GITLAB_API_URL`、`GITLAB_PERMISSION_MODE`、`GITLAB_TOOLSETS` 和 `GITLAB_MASKING_ENABLED`。无需另外安装 Node.js 或 npm。
146
+
147
+ 如需从源码构建 bundle,运行 `npm run pack:mcpb`(生成 `gitlab-mcp.mcpb`)。
142
148
 
143
149
  #### 使用 CLI 参数(适用于环境变量有问题的客户端)
144
150
 
package/build/index.js CHANGED
@@ -109,6 +109,7 @@ import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./u
109
109
  import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
110
110
  import { applyJmespathToToolResult, getJmespathSyntaxError, JMESPATH_TOOL_ARGUMENT, JMESPATH_TOOL_ARGUMENT_DESCRIPTION, omitJmespathArgument, readJmespathExpression, } from "./utils/jmespath-tool-result.js";
111
111
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
112
+ import { zodSchemaFor } from "./utils/schema.js";
112
113
  import { GitLabClientPool } from "./gitlab-client-pool.js";
113
114
  import { advertisesReadOnly, allTools, readOnlyTools, destructiveTools, deleteTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, findSmallestToolsetForTool, isExplicitOnlyToolsetId, isExplicitToolsetOnlyTool, isToolsetFullyActive, listDiscoverableCategories, TOOLSET_DEFINITIONS, DISCOVERABLE_TOOLSET_IDS, } from "./tools/registry.js";
114
115
  import { executeGitLabApiRequestAsync, GitLabApiRequestHttpError, GitLabApiRequestInputError, } from "./tools/gitlab-api-request.js";
@@ -6362,6 +6363,74 @@ async function erasePipelineJob(projectId, jobId) {
6362
6363
  const TERMINAL_PIPELINE_STATUSES = new Set(["success", "failed", "canceled", "skipped", "manual"]);
6363
6364
  const TERMINAL_JOB_STATUSES = new Set(["success", "failed", "canceled", "skipped", "manual"]);
6364
6365
  const TERMINAL_REBASE_STATUSES = new Set(["finished"]);
6366
+ // GitLab accepts PUT .../rebase before Sidekiq starts the job. Until then, GET
6367
+ // with include_rebase_in_progress can still say rebase_in_progress=false and return
6368
+ // the pre-rebase sha (gitlab-org/gitlab-foss#57481). Recheck that snapshot a few
6369
+ // times, capped so the default 5s poll interval cannot stretch this into 15s.
6370
+ const REBASE_UNCHANGED_HEAD_RECHECK_LIMIT = 3;
6371
+ const REBASE_UNCHANGED_HEAD_RECHECK_INTERVAL_CAP_MS = 1000;
6372
+ function rebaseHeadMoved(previous, current) {
6373
+ return current.sha !== previous.sha;
6374
+ }
6375
+ function rebaseFailureExplained(previous, current) {
6376
+ const mergeError = current.merge_error;
6377
+ if (typeof mergeError !== "string" || mergeError.length === 0) {
6378
+ return false;
6379
+ }
6380
+ return mergeError !== previous.merge_error;
6381
+ }
6382
+ function delayForRebasePoll(delayMs) {
6383
+ return new Promise(resolve => {
6384
+ setTimeout(resolve, delayMs);
6385
+ });
6386
+ }
6387
+ async function getMergeRequestRebaseStatusBeforeDeadline(projectId, mergeRequestIid, deadline) {
6388
+ const remaining = deadline - Date.now();
6389
+ if (remaining <= 0) {
6390
+ throw new Error("Timed out waiting for terminal status");
6391
+ }
6392
+ return getMergeRequestRebaseStatus(projectId, mergeRequestIid, AbortSignal.timeout(remaining));
6393
+ }
6394
+ /**
6395
+ * Poll until a rebase finishes, without trusting a finished snapshot that still
6396
+ * has the pre-rebase sha and no new merge_error. A later rebase_in_progress true
6397
+ * means the job did start, so wait for that terminal status as usual. Rechecks
6398
+ * share the caller's timeout and never add time beyond it.
6399
+ */
6400
+ async function waitForMergeRequestRebase(projectId, mergeRequestIid, previous, timeoutSeconds = 300, pollIntervalSeconds = 5) {
6401
+ const deadline = Date.now() + timeoutSeconds * 1000;
6402
+ let snapshot = await getMergeRequestRebaseStatusBeforeDeadline(projectId, mergeRequestIid, deadline);
6403
+ let rechecksRemaining = REBASE_UNCHANGED_HEAD_RECHECK_LIMIT;
6404
+ while (true) {
6405
+ if (!TERMINAL_REBASE_STATUSES.has(snapshot.status)) {
6406
+ rechecksRemaining = 0;
6407
+ const remaining = deadline - Date.now();
6408
+ if (remaining <= 0) {
6409
+ break;
6410
+ }
6411
+ await delayForRebasePoll(Math.min(pollIntervalSeconds * 1000, remaining));
6412
+ snapshot = await getMergeRequestRebaseStatusBeforeDeadline(projectId, mergeRequestIid, deadline);
6413
+ continue;
6414
+ }
6415
+ const mergeRequest = snapshot.mergeRequest;
6416
+ if (rebaseHeadMoved(previous, mergeRequest) || rebaseFailureExplained(previous, mergeRequest)) {
6417
+ return mergeRequest;
6418
+ }
6419
+ if (rechecksRemaining <= 0) {
6420
+ return mergeRequest;
6421
+ }
6422
+ const remaining = deadline - Date.now();
6423
+ const delay = Math.min(pollIntervalSeconds * 1000, REBASE_UNCHANGED_HEAD_RECHECK_INTERVAL_CAP_MS, remaining);
6424
+ // The next poll has to land inside the timeout. If one full backoff does not fit, stop.
6425
+ if (remaining <= 0 || delay >= remaining) {
6426
+ return mergeRequest;
6427
+ }
6428
+ rechecksRemaining -= 1;
6429
+ await delayForRebasePoll(delay);
6430
+ snapshot = await getMergeRequestRebaseStatusBeforeDeadline(projectId, mergeRequestIid, deadline);
6431
+ }
6432
+ throw new Error(`Timed out waiting for terminal status; last status: ${snapshot.status}`);
6433
+ }
6365
6434
  async function waitForStatus(fetchStatus, terminalStatuses, timeoutSeconds = 300, pollIntervalSeconds = 5) {
6366
6435
  const deadline = Date.now() + timeoutSeconds * 1000;
6367
6436
  const fetchBeforeDeadline = async () => {
@@ -8764,12 +8833,19 @@ async function handleToolCall(params) {
8764
8833
  }
8765
8834
  case "rebase_merge_request": {
8766
8835
  const { project_id, merge_request_iid, skip_ci, wait, timeout_seconds, poll_interval_seconds, } = RebaseMergeRequestSchema.parse(params.arguments);
8767
- const enqueued = await rebaseMergeRequest(project_id, merge_request_iid, skip_ci);
8768
8836
  if (!wait) {
8837
+ const enqueued = await rebaseMergeRequest(project_id, merge_request_iid, skip_ci);
8769
8838
  return { content: [{ type: "text", text: JSON.stringify(enqueued) }] };
8770
8839
  }
8771
- const { mergeRequest } = await waitForStatus(signal => getMergeRequestRebaseStatus(project_id, merge_request_iid, signal), TERMINAL_REBASE_STATUSES, timeout_seconds, poll_interval_seconds);
8772
- return { content: [{ type: "text", text: JSON.stringify(mergeRequest) }] };
8840
+ const { mergeRequest: previous } = await getMergeRequestRebaseStatus(project_id, merge_request_iid);
8841
+ await rebaseMergeRequest(project_id, merge_request_iid, skip_ci);
8842
+ const mergeRequest = await waitForMergeRequestRebase(project_id, merge_request_iid, previous, timeout_seconds, poll_interval_seconds);
8843
+ const rebaseResult = {
8844
+ ...mergeRequest,
8845
+ previous_sha: previous.sha,
8846
+ head_changed: rebaseHeadMoved(previous, mergeRequest),
8847
+ };
8848
+ return { content: [{ type: "text", text: JSON.stringify(rebaseResult) }] };
8773
8849
  }
8774
8850
  case "get_merge_request_approval_state": {
8775
8851
  const args = GetMergeRequestApprovalStateSchema.parse(params.arguments);
@@ -11328,9 +11404,17 @@ async function handleToolCall(params) {
11328
11404
  // Sensitive fields are also covered by REDACT_PATHS if arguments are logged elsewhere.
11329
11405
  logger.debug({ tool: params.name }, "Tool call failed");
11330
11406
  if (error instanceof z.ZodError) {
11331
- throw new Error(`Invalid arguments: ${error.issues
11332
- .map(e => `${e.path.join(".")}: ${e.message}`)
11333
- .join(", ")}`);
11407
+ const detail = error.issues
11408
+ .map(e => (e.path.length > 0 ? `${e.path.join(".")}: ${e.message}` : e.message))
11409
+ .join("; ");
11410
+ // Response schemas throw ZodError too. If the arguments pass the tool's own
11411
+ // schema, the failure came from parsing a GitLab response.
11412
+ const argsSchema = zodSchemaFor(allTools.find(tool => tool.name === params.name)?.inputSchema);
11413
+ if (argsSchema?.safeParse(params.arguments).success) {
11414
+ throw new Error(`Unexpected GitLab response for ${params.name}: ${detail}`);
11415
+ }
11416
+ // ToolInputError is returned as an isError tool result, so the model can fix and retry.
11417
+ throw new ToolInputError(`Invalid arguments for ${params.name}: ${detail}`);
11334
11418
  }
11335
11419
  throw error;
11336
11420
  }
@@ -2,7 +2,7 @@ const TOOL_GUIDANCE = {
2
2
  merge_merge_request: "Use this only after checking the merge request approval, conflict, and pipeline state; use `approve_merge_request` to approve rather than merge. The operation changes repository state and may squash commits, schedule auto-merge, or delete the source branch, so it requires merge permission and returns GitLab's merge result or a mergeability error. Pass `sha` from `get_merge_request` (`sha` or `diff_refs.head_sha`); GitLab 19.2+ groups may reject merges without it.",
3
3
  approve_merge_request: "Use this to record an approval on an existing merge request; it does not merge the request or change its source branch. The operation changes review state, may require re-authentication or approval permission, and returns the updated approval result or a permission/state error.",
4
4
  unapprove_merge_request: "Use this to remove the current user's approval from an existing merge request; use `merge_merge_request` only when you intend to merge. The operation changes review state and requires approval permission, and GitLab returns the updated result or an error when the request or approval is unavailable.",
5
- rebase_merge_request: "Use this when the source branch is behind its target branch; use `get_merge_request_conflicts` to inspect conflicts and `merge_merge_request` to merge. The operation rewrites the source branch history with a force push and requires permission to push to that branch. With `wait` true (default), it polls until the rebase ends and returns the merge request: GitLab reports a failed rebase in `merge_error`, but that field can also keep a message from an earlier attempt, so compare `sha` with its value before the rebase. If the rebase still runs after `timeout_seconds`, it returns a timeout error while GitLab continues the rebase. With `wait` false, it returns GitLab's enqueue response (`rebase_in_progress` true) at once. Set `skip_ci` to skip the CI pipeline for the rebased branch.",
5
+ rebase_merge_request: "Use this when the source branch is behind its target branch; use `get_merge_request_conflicts` to inspect conflicts and `merge_merge_request` to merge. The operation rewrites the source branch history with a force push and requires permission to push to that branch. With `wait` true (default), it polls until the rebase ends and returns the merge request plus `previous_sha` (its `sha` before the rebase) and `head_changed`. `head_changed: false` means that the head did not move; `merge_error` gives the reason when GitLab sends one. `merge_error` can keep a message from an earlier attempt, so it alone does not prove that this attempt failed. If the rebase still runs after `timeout_seconds`, it returns a timeout error while GitLab continues the rebase. With `wait` false, it returns GitLab's enqueue response (`rebase_in_progress` true) at once. Set `skip_ci` to skip the CI pipeline for the rebased branch.",
6
6
  get_merge_request_approval_state: "Use this to inspect approval rules and approvers before deciding whether a merge request can be merged; use `approve_merge_request` to change approval state. It is read-only and returns the approval-state response, while missing requests, unsupported GitLab versions, and permission failures are reported as errors.",
7
7
  get_merge_request_conflicts: "Use this to inspect merge conflicts before attempting `merge_merge_request`; it reports conflicts and does not resolve them. It is read-only, requires access to the project and merge request, and returns GitLab's conflict data or an error when the request cannot be evaluated.",
8
8
  list_merge_request_pipelines: "Use this to inspect pipelines associated with one merge request; use `list_pipelines` for project-wide pipeline filtering. It is read-only and paginated, requires project access, and returns pipeline records or GitLab errors for invalid identifiers, missing resources, or rate limits.",
@@ -1,4 +1,10 @@
1
1
  import { z } from "zod";
2
+ // JSON Schema produced by toJSONSchema -> the Zod schema it came from.
3
+ const zodSchemaByJsonSchema = new WeakMap();
4
+ /**
5
+ * Returns the Zod schema a toJSONSchema() result was built from, if any.
6
+ */
7
+ export const zodSchemaFor = (jsonSchema) => jsonSchema && typeof jsonSchema === "object" ? zodSchemaByJsonSchema.get(jsonSchema) : undefined;
2
8
  /**
3
9
  * Convert a Zod schema to JSON Schema, fixing nullable/optional fields
4
10
  * so they are not marked as required, and extracting required fields from the Zod schema.
@@ -154,5 +160,6 @@ export const toJSONSchema = (schema) => {
154
160
  }
155
161
  }
156
162
  }
163
+ zodSchemaByJsonSchema.set(fixedSchema, schema);
157
164
  return fixedSchema;
158
165
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
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": [
@@ -69,6 +69,7 @@
69
69
  "lint:fix": "eslint . --fix",
70
70
  "release": "bash scripts/release.sh",
71
71
  "release:mcp-registry": "bash scripts/publish_mcp_registry.sh",
72
+ "pack:mcpb": "npm run build && bash scripts/pack-mcpb.sh",
72
73
  "format": "prettier --write \"**/*.{js,ts,json,md}\"",
73
74
  "format:check": "prettier --check \"**/*.{js,ts,json,md}\""
74
75
  },