@zereight/mcp-gitlab 2.3.8-next.145 → 2.3.8-next.147

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.zh-CN.md CHANGED
@@ -1,30 +1,132 @@
1
1
  # GitLab MCP Server
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@zereight/mcp-gitlab.svg)](https://www.npmjs.com/package/@zereight/mcp-gitlab)
4
- [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_GitLab_MCP-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=zereight.gitlab-mcp&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22gitlab-token%22%2C%22description%22%3A%22GitLab%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40zereight%2Fmcp-gitlab%40latest%22%5D%2C%22env%22%3A%7B%22GITLAB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agitlab-token%7D%22%2C%22GITLAB_API_URL%22%3A%22https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%22%7D%7D)
5
- [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=zereight.gitlab-mcp&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB6ZXJlaWdodC9tY3AtZ2l0bGFiQGxhdGVzdCJdLCJlbnYiOnsiR0lUTEFCX1BFUlNPTkFMX0FDQ0VTU19UT0tFTiI6IiR7ZW52OkdJVExBQl9QRVJTT05BTF9BQ0NFU1NfVE9LRU59IiwiR0lUTEFCX0FQSV9VUkwiOiJodHRwczovL2dpdGxhYi5jb20vYXBpL3Y0In19)
6
4
 
7
5
  [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [Português (Brasil)](./README.pt-BR.md)
8
6
 
9
- 📖 **[文档 →](https://zereight.github.io/gitlab-mcp/)** 设置指南、环境变量和完整工具参考请查看托管文档站点。
7
+ 📖 **[文档 →](https://zereight.github.io/gitlab-mcp/)** · [llms.txt](./llms.txt) · [llms-full.txt](./llms-full.txt)
10
8
 
11
- 下面是 `@zereight/mcp-gitlab` 的推荐本地 PAT 安装。命令参数与 [Claude Code](./docs/clients/claude-code.md) 和 [Codex](./docs/clients/codex.md) 指南相同。
9
+ **默认暴露 40 个工具**(38 个 `core` + `discover_tools` + `help`)。总共 278 个(toolset 中 275 个 + `execute_graphql` + `discover_tools` + `help`)。先用精简集,再在本会话中用 `discover_tools` 扩展。
12
10
 
13
11
  先安装 `zereight-mcp-gitlab` 二进制。`npm install -g @zereight/mcp-gitlab`,或 Homebrew `brew tap zereight/gitlab-mcp https://github.com/zereight/gitlab-mcp` 然后 `brew install zereight/gitlab-mcp/zereight-mcp-gitlab`。官方 MCP Registry 条目:[`io.github.zereight/gitlab-mcp`](https://registry.modelcontextprotocol.io/?q=io.github.zereight%2Fgitlab-mcp)。一键安装:`npx -y @zereight/mcp-gitlab@latest`。
14
12
 
15
- 先不回显地读取令牌,再 export。shell 历史里只保留变量名。
13
+ ### Without → With
14
+
15
+ | 没有此 MCP | 有此 MCP |
16
+ | ---------- | -------- |
17
+ | 把 MR URL 和 diff 粘贴进聊天 | `list_merge_request_changed_files` → 批量 `get_merge_request_file_diff` |
18
+ | 在 GitLab UI 里翻 job 日志 | 带分页的 `get_pipeline_job_output` |
19
+ | 每一轮都重新说明项目路径 | `project_id=group%2Fproject` 和 `merge_request_iid=123` |
20
+
21
+ ### 三条安装路径
22
+
23
+ | 路径 | 适用 | 方式 |
24
+ | ---- | ---- | ---- |
25
+ | 本地 IDE | 本机上的 Cursor、Claude Code、Codex | stdio + PAT 或本地 OAuth |
26
+ | 共享 HTTP | 团队 / 托管 MCP | Streamable HTTP URL + 远程授权或 MCP OAuth |
27
+ | Claude.ai | Claude 网页连接器 | 公网 HTTPS + `GITLAB_MCP_OAUTH` |
28
+
29
+ ### 安装
30
+
31
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=zereight.gitlab-mcp&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB6ZXJlaWdodC9tY3AtZ2l0bGFiQGxhdGVzdCJdLCJlbnYiOnsiR0lUTEFCX1BFUlNPTkFMX0FDQ0VTU19UT0tFTiI6IiR7ZW52OkdJVExBQl9QRVJTT05BTF9BQ0NFU1NfVE9LRU59IiwiR0lUTEFCX0FQSV9VUkwiOiJodHRwczovL2dpdGxhYi5jb20vYXBpL3Y0In19)
32
+
33
+ 先安装二进制(`npm install -g @zereight/mcp-gitlab`,或 Homebrew `brew tap zereight/gitlab-mcp https://github.com/zereight/gitlab-mcp` 然后 `brew install zereight/gitlab-mcp/zereight-mcp-gitlab`)。不回显地读取令牌:
16
34
 
17
35
  ```bash
18
36
  read -rs GITLAB_PERSONAL_ACCESS_TOKEN && export GITLAB_PERSONAL_ACCESS_TOKEN
19
37
  ```
20
38
 
21
- | 客户端 | 安装 |
22
- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
23
- | Claude Code | `claude mcp add gitlab --transport stdio --scope local --env GITLAB_PERSONAL_ACCESS_TOKEN="$GITLAB_PERSONAL_ACCESS_TOKEN" --env GITLAB_API_URL=https://gitlab.com/api/v4 -- zereight-mcp-gitlab` |
24
- | Codex | `codex mcp add gitlab --env GITLAB_PERSONAL_ACCESS_TOKEN="$GITLAB_PERSONAL_ACCESS_TOKEN" --env GITLAB_API_URL=https://gitlab.com/api/v4 -- zereight-mcp-gitlab` |
25
- | Cursor | [Cursor 设置指南](./docs/clients/cursor.md) |
26
- | VS Code | [VS Code 设置指南](./docs/clients/vscode.md) |
27
- | JSON 客户端 | [基于 JSON 的 MCP 客户端设置指南](./docs/clients/json-clients.md) |
39
+ ```bash
40
+ # Claude Code
41
+ claude mcp add gitlab --transport stdio --scope local --env GITLAB_PERSONAL_ACCESS_TOKEN="$GITLAB_PERSONAL_ACCESS_TOKEN" --env GITLAB_API_URL=https://gitlab.com/api/v4 -- zereight-mcp-gitlab
42
+
43
+ # Codex
44
+ codex mcp add gitlab --env GITLAB_PERSONAL_ACCESS_TOKEN="$GITLAB_PERSONAL_ACCESS_TOKEN" --env GITLAB_API_URL=https://gitlab.com/api/v4 -- zereight-mcp-gitlab
45
+ ```
46
+
47
+ 其他客户端:[VS Code](./docs/clients/vscode.md) · [GitHub Copilot](./docs/clients/copilot.md) · [JSON 客户端](./docs/clients/json-clients.md) · [Cursor 详情](./docs/clients/cursor.md)
48
+
49
+ ### 代理粘贴提示
50
+
51
+ 把下面内容粘贴给能改 MCP 配置的代理:
52
+
53
+ ```
54
+ 为此工作区安装 GitLab MCP 服务器(@zereight/mcp-gitlab)。
55
+ 二进制:zereight-mcp-gitlab(npm i -g @zereight/mcp-gitlab,或 npx -y @zereight/mcp-gitlab@latest)。
56
+ 认证:GITLAB_PERSONAL_ACCESS_TOKEN 和 GITLAB_API_URL=https://gitlab.com/api/v4(或 GITLAB_USE_OAUTH=true)。
57
+ 本地:claude mcp add gitlab --transport stdio --scope local --env GITLAB_PERSONAL_ACCESS_TOKEN="$GITLAB_PERSONAL_ACCESS_TOKEN" --env GITLAB_API_URL=https://gitlab.com/api/v4 -- zereight-mcp-gitlab
58
+ 或:codex mcp add gitlab --env GITLAB_PERSONAL_ACCESS_TOKEN="$GITLAB_PERSONAL_ACCESS_TOKEN" --env GITLAB_API_URL=https://gitlab.com/api/v4 -- zereight-mcp-gitlab
59
+ Cursor:使用 README deeplink 或 docs/clients/cursor.md。
60
+ 远程 HTTP:将客户端指向 https://mcp.example.com/mcp(Streamable HTTP)。Claude.ai 需要该主机启用 GITLAB_MCP_OAUTH。
61
+ 默认列表:40 个工具(38 个 core + discover_tools + help)。用 discover_tools 扩展。除非接受绕过风险,否则不要启用 api toolset。
62
+ 同等非 MCP 路径:npx skills add zereight/gitlab-mcp --skill gitlab-mcp-skill 加上 zereight-mcp-gitlab CLI。
63
+ ```
64
+
65
+ ### 远程 HTTP
66
+
67
+ 托管或自建服务器的一等选项。把客户端指向 Streamable HTTP URL:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "gitlab": {
73
+ "type": "http",
74
+ "url": "https://mcp.example.com/mcp"
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ - 托管 / Claude.ai:服务器上设置 `GITLAB_MCP_OAUTH=true`(见 [MCP OAuth](#使用-mcp-oauth-代理gitlab_mcp_oauth))
81
+ - 每用户 PAT 头:`REMOTE_AUTHORIZATION=true`(见 [远程授权](#使用远程授权remote_authorization))
82
+ - 本地 Docker 示例见 [使用方法](#使用方法)
83
+
84
+ ### Skill + CLI(不需要 MCP)
85
+
86
+ Skill + CLI 是同等路径,不是备选。同样的工作流,终端里拿完整载荷,无需注册 MCP。
87
+
88
+ ```bash
89
+ npx skills add zereight/gitlab-mcp --skill gitlab-mcp-skill
90
+ zereight-mcp-gitlab user whoami
91
+ zereight-mcp-gitlab mr list
92
+ ```
93
+
94
+ 见 [不使用 MCP 的 CLI](./docs/getting-started/cli-without-mcp.md) 和 [`skills/gitlab-mcp/`](./skills/gitlab-mcp/)。
95
+
96
+ ### MCP vs Human CLI
97
+
98
+ | 使用 MCP 当 | 使用 CLI 当 |
99
+ | ----------- | ----------- |
100
+ | 代理需要聊天里的结构化工具 | 需要完整载荷(diff、日志、产物) |
101
+ | `jmespath` 或 compact 结果已够用 | 已开 `GITLAB_MCP_COMPACT_RESULTS`,其余要在终端看 |
102
+ | 已经注册了 MCP 服务器 | 已有 PAT 或跑过 `auth`,不想要 MCP |
103
+
104
+ Human CLI 从不 compact。破坏性工具和 `GITLAB_TOOL_POLICY_APPROVE` 工具需要 `--yes`。
105
+
106
+ ### Tips
107
+
108
+ `project_id` 是数字 ID 或 URL 编码路径。例如:组 `group`、项目 `project`、合并请求 `!123` → `project_id=group%2Fproject`,`merge_request_iid=123`。嵌套组的每一段之间用 `%2F`(`group%2Fsubgroup%2Fproject`)。
109
+
110
+ ### FAQ
111
+
112
+ **如何认证?** 本地桌面最简单的是 `GITLAB_PERSONAL_ACCESS_TOKEN`(或 `GITLAB_PERSONAL_ACCESS_TOKEN_FILE`)。本地 OAuth 用 `GITLAB_USE_OAUTH=true`。远程 Claude.ai 用 `GITLAB_MCP_OAUTH`。多用户 HTTP 用 `REMOTE_AUTHORIZATION` 和每请求令牌头。
113
+
114
+ **为什么只看到大约 40 个工具?** 默认 `core` toolset 有 38 个工具,外加始终开启的 `discover_tools` 和 `help`。不带参数调用 `discover_tools` 可列出类别,再传入类别(`pipelines`、`wiki` 等)即可在本会话激活。`GITLAB_TOOLSETS=all` 仍然**不会**启用 `api`。
115
+
116
+ **本地 OAuth、MCP OAuth 和远程授权有何区别?** 本地 OAuth 在服务器同一台机器上打开浏览器。MCP OAuth 用于公网 HTTPS 服务器和 Claude.ai 这类客户端。远程授权转发调用者的 PAT 或 job token,没有 OAuth 流程。
117
+
118
+ **如何缩小巨大响应?** 在任意工具调用上传递 `jmespath`。或设置 `GITLAB_MCP_COMPACT_RESULTS=true`,把过大的 MCP 回复变成预览加 CLI 命令。需要完整载荷时用 CLI。
119
+
120
+ **为什么 `api` toolset 危险?** `gitlab_api_request` 会绕过组/项目允许列表、其他工具过滤和单工具安全检查。提示注入可以驱动它。只有在 `GITLAB_TOOLSETS` 中列出 `api` 时才启用。`all`、`discover_tools` 和 `GITLAB_TOOLS` 都不会启用它。
121
+
122
+ ### Cursor rule / CLAUDE.md 片段
123
+
124
+ ```
125
+ GitLab 合并请求、议题和流水线走此 MCP(@zereight/mcp-gitlab)。
126
+ 从默认 core 工具开始,用 discover_tools 扩展。
127
+ 优先 list_merge_request_changed_files,再批量 get_merge_request_file_diff。
128
+ 结果用 jmespath 或 compact;完整 diff 和 job 日志用 CLI。
129
+ ```
28
130
 
29
131
  [![Star History Chart](./assets/star-history.png)](https://www.star-history.com/?repos=zereight%2Fgitlab-mcp&type=date&legend=top-left)
30
132
 
@@ -36,7 +138,7 @@ read -rs GITLAB_PERSONAL_ACCESS_TOKEN && export GITLAB_PERSONAL_ACCESS_TOKEN
36
138
 
37
139
  ### 为什么使用这个 GitLab MCP?
38
140
 
39
- - **277 个工具:toolset 中 275 个 + `execute_graphql` + `discover_tools`** — 从小型 toolset 开始,运行时按需激活类别
141
+ - **默认 40 个工具(38 个 `core` + `discover_tools` + `help`);共 278 个** — 先精简,再用 `discover_tools` 在本会话扩展
40
142
  - **MR 两步审查** — `list_merge_request_changed_files` → 批量 `get_merge_request_file_diff`
41
143
  - **内置 Agent Skill** — `skills/gitlab-mcp/` 工作流指南
42
144
  - **认证灵活** — Personal Access Token、本地 OAuth2 浏览器流程、MCP OAuth 代理、按请求远程授权
@@ -50,7 +152,7 @@ read -rs GITLAB_PERSONAL_ACCESS_TOKEN && export GITLAB_PERSONAL_ACCESS_TOKEN
50
152
  | | @zereight/mcp-gitlab | GitLab MCP A(社区 CQRS 型) | GitLab 官方 MCP 服务器 |
51
153
  | ------------ | -------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
52
154
  | **更适合** | AI 代理工作流 | 企业多实例 / 分组工具 | 内置于 GitLab,无需单独安装 |
53
- | **工具模型** | 277 个工具:toolset 中 275 个 + `execute_graphql` + `discover_tools` | ~50–60 个 `browse_*` / `manage_*` 分组工具 | 用 HTTP 头选择 toolset |
155
+ | **工具模型** | 278 个工具:toolset 中 275 个 + `execute_graphql` + `discover_tools` + `help` | ~50–60 个 `browse_*` / `manage_*` 分组工具 | 用 HTTP 头选择 toolset |
54
156
  | **MR 审查** | 两步批量 diff | 因服务器而异 | `get_merge_request` 带 diffs |
55
157
  | **Node.js** | >=18.17 | 通常 >=24 | stdio 经 `mcp-remote`,需要 Node.js 20+ |
56
158
  | **许可证** | MIT | 因服务器而异 | GitLab 的一部分 |
@@ -602,7 +704,7 @@ node build/index.js
602
704
 
603
705
  ## Agent Skill Files
604
706
 
605
- 对于支持 skill/instruction 加载的 AI 代理(Claude Code、GitHub Copilot、Cursor 等),[`skills/gitlab-mcp/`](./skills/gitlab-mcp/) 中提供了预构建 skill 文件。
707
+ Skill + CLI 与 MCP 同等(见 [Skill + CLI](#skill--cli不需要-mcp))。对于支持 skill/instruction 加载的 AI 代理(Claude Code、GitHub Copilot、Cursor 等),[`skills/gitlab-mcp/`](./skills/gitlab-mcp/) 中提供了预构建 skill 文件。
606
708
 
607
709
  - **[SKILL.md](./skills/gitlab-mcp/SKILL.md)** — 核心指南,包含工具集概览、关键工作流和参数提示
608
710
  - **[reference/](./skills/gitlab-mcp/reference/)** — 代码审查、合并请求、议题、流水线和漏洞分类的详细工作流文档
package/build/index.js CHANGED
@@ -94,6 +94,8 @@ import { formatPrometheusMetrics } from "./server/metrics.js";
94
94
  import { SERVER_VERSION } from "./server/version.js";
95
95
  import { listMcpResources, listMcpResourceTemplates, readMcpResource, } from "./server/mcp-resources.js";
96
96
  import { getMcpPrompt, listMcpPrompts } from "./server/mcp-prompts.js";
97
+ import { MCP_INSTRUCTIONS } from "./server/mcp-instructions.js";
98
+ import { formatUnknownHelpTopic, getWorkflowGuide, readHelpTopic, } from "./server/mcp-help.js";
97
99
  import { isInitializationRequestBody, isUnauthenticatedDiscoveryRequestBody, readAcceptHeader, readMcpSessionIdHeader, redactSessionIdForLog, } from "./server/request-helpers.js";
98
100
  export { readMcpSessionIdHeader } from "./server/request-helpers.js";
99
101
  import { apiUrlUserinfoWarning, commaSplitHint, configuredApiUrlErrors, encodeGitLabPath, encodeGitLabPathSegment, invalidListEntryError, isCleartextRemoteUrl, normalizeGitLabApiUrl, parseConfiguredGitLabApiUrls, } from "./utils/url.js";
@@ -114,6 +116,8 @@ import { parseGitLabVersionApiResponse, } from "./utils/gitlab-instance-version.
114
116
  import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
115
117
  import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
116
118
  import { applyJmespathToToolResult, getJmespathSyntaxError, JMESPATH_TOOL_ARGUMENT, JMESPATH_TOOL_ARGUMENT_DESCRIPTION, omitJmespathArgument, readJmespathExpression, } from "./utils/jmespath-tool-result.js";
119
+ import { appendRecoveryHint } from "./utils/error-hints.js";
120
+ import { applyListSummaryToToolResult, omitListSummaryArgument, readListSummaryRequested, toolAcceptsDecoratedSummary, } from "./utils/list-summary.js";
117
121
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
118
122
  import { zodSchemaFor } from "./utils/schema.js";
119
123
  import { GitLabClientPool } from "./gitlab-client-pool.js";
@@ -211,6 +215,7 @@ function createServer() {
211
215
  prompts: {},
212
216
  resources: {},
213
217
  },
218
+ instructions: MCP_INSTRUCTIONS,
214
219
  });
215
220
  mcpServer.server.setRequestHandler(ListPromptsRequestSchema, async () => listMcpPrompts());
216
221
  mcpServer.server.setRequestHandler(GetPromptRequestSchema, async (request) => getMcpPrompt(request.params.name, (request.params.arguments ?? {})));
@@ -229,14 +234,18 @@ function createServer() {
229
234
  const toolName = request.params.name;
230
235
  const start = Date.now();
231
236
  let maskingEngine;
232
- const jmespathExpression = readJmespathExpression(request.params.arguments);
237
+ const requestArguments = readOwnRecord(request.params.arguments);
238
+ const jmespathExpression = readJmespathExpression(requestArguments === undefined ? undefined : { ...requestArguments });
239
+ const listSummaryRequested = toolAcceptsDecoratedSummary(toolName) &&
240
+ readListSummaryRequested(requestArguments === undefined ? undefined : { ...requestArguments });
233
241
  const logCompletion = (result, completionOptions) => {
234
242
  const durationMs = Date.now() - start;
235
243
  logger.info({ tool: toolName, event: "tool_call_done", durationMs }, `tool_call_done: ${toolName} (${durationMs}ms)`);
236
244
  const isPlainTextResult = toolName === "get_pipeline_job_output" ||
237
245
  toolName === "get_job_artifact_file" ||
238
246
  (toolName === "download_release_asset" && !IS_REMOTE);
239
- const filtered = applyJmespathToToolResult(redactErrorResultUrls(result), jmespathExpression, {
247
+ const summarized = applyListSummaryToToolResult(redactErrorResultUrls(result), listSummaryRequested);
248
+ const filtered = applyJmespathToToolResult(summarized, jmespathExpression, {
240
249
  ...completionOptions,
241
250
  maskValue: maskingEngine
242
251
  ? (value) => maskingEngine.maskValue(value)
@@ -331,6 +340,23 @@ function createServer() {
331
340
  !filteredTools.some(t => t.name === toolName)) {
332
341
  throw new Error(buildToolUnavailableMessage(toolName));
333
342
  }
343
+ if (toolName === "help") {
344
+ const topic = readHelpTopic(request.params.arguments?.topic);
345
+ if (topic === "invalid") {
346
+ return logCompletion({
347
+ content: [
348
+ {
349
+ type: "text",
350
+ text: formatUnknownHelpTopic(request.params.arguments?.topic),
351
+ },
352
+ ],
353
+ isError: true,
354
+ });
355
+ }
356
+ return logCompletion({
357
+ content: [{ type: "text", text: getWorkflowGuide(topic) }],
358
+ });
359
+ }
334
360
  // Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
335
361
  if (toolName === "discover_tools") {
336
362
  const category = request.params.arguments?.category?.trim()?.toLowerCase();
@@ -477,8 +503,13 @@ function createServer() {
477
503
  const { _confirmed, ...cleanArgs } = request.params.arguments || {};
478
504
  request.params.arguments = cleanArgs;
479
505
  }
480
- if (request.params.arguments && typeof request.params.arguments === "object") {
481
- request.params.arguments = omitJmespathArgument(request.params.arguments);
506
+ const ownedArguments = readOwnRecord(request.params.arguments);
507
+ if (ownedArguments) {
508
+ let cleanedArguments = omitJmespathArgument({ ...ownedArguments });
509
+ if (toolAcceptsDecoratedSummary(toolName)) {
510
+ cleanedArguments = omitListSummaryArgument(cleanedArguments);
511
+ }
512
+ request.params.arguments = cleanedArguments;
482
513
  }
483
514
  if (sessionContext) {
484
515
  // Policy selection and the tool handler must use the same request-local
@@ -500,7 +531,7 @@ function createServer() {
500
531
  content: [
501
532
  {
502
533
  type: "text",
503
- text: safeError instanceof Error ? safeError.message : String(safeError),
534
+ text: appendRecoveryHint(safeError instanceof Error ? safeError.message : String(safeError)),
504
535
  },
505
536
  ],
506
537
  isError: true,
@@ -1418,12 +1449,12 @@ async function handleGitLabError(response) {
1418
1449
  // before the common tool-call boundary returns/logs it.
1419
1450
  logger.error("GitLab API Rate Limit Exceeded");
1420
1451
  logger.error("User API Key Rate limit exceeded. Please try again later.");
1421
- const error = new GitLabApiError(`GitLab API Rate Limit Exceeded: ${errorBody}`);
1452
+ const error = new GitLabApiError(appendRecoveryHint(`GitLab API Rate Limit Exceeded: ${errorBody}`));
1422
1453
  throw error;
1423
1454
  }
1424
1455
  else {
1425
1456
  // Handle other API errors
1426
- const error = new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1457
+ const error = new GitLabApiError(appendRecoveryHint(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`));
1427
1458
  throw error;
1428
1459
  }
1429
1460
  }
@@ -7646,7 +7677,7 @@ function getSafeAttachmentFilename(filename) {
7646
7677
  }
7647
7678
  return filename;
7648
7679
  }
7649
- async function downloadAttachment(projectId, secret, filename, localPath) {
7680
+ async function downloadAttachment(projectId, secret, filename, localPath, metadataOnly = false) {
7650
7681
  const effectiveProjectId = getEffectiveProjectId(projectId);
7651
7682
  const safeFilename = getSafeAttachmentFilename(filename);
7652
7683
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/uploads/${encodeGitLabPathSegment(secret)}/${encodeGitLabPathSegment(safeFilename)}`);
@@ -7655,6 +7686,18 @@ async function downloadAttachment(projectId, secret, filename, localPath) {
7655
7686
  await handleGitLabError(response);
7656
7687
  }
7657
7688
  const mimeType = getImageMimeType(safeFilename);
7689
+ if (metadataOnly) {
7690
+ const headerLength = Number(response.headers.get("content-length"));
7691
+ if (response.body) {
7692
+ await response.body.cancel();
7693
+ }
7694
+ return {
7695
+ buffer: Buffer.alloc(0),
7696
+ filename: safeFilename,
7697
+ mimeType,
7698
+ byteLength: Number.isFinite(headerLength) ? headerLength : undefined,
7699
+ };
7700
+ }
7658
7701
  // Non-image files, and images with local_path, are saved as new files under the
7659
7702
  // download directory. Upload names and local_path come from untrusted content, so
7660
7703
  // dot names (.git, .vscode, .envrc) are refused and existing files are never replaced.
@@ -10718,10 +10761,27 @@ async function handleToolCall(params) {
10718
10761
  };
10719
10762
  }
10720
10763
  // Readonly mode never writes to disk: images still come back inline.
10721
- if (GITLAB_PERMISSION_MODE === "readonly" && (!mimeType || args.local_path)) {
10764
+ if (GITLAB_PERMISSION_MODE === "readonly" &&
10765
+ !args.metadata_only &&
10766
+ (!mimeType || args.local_path)) {
10722
10767
  throw new Error("download_attachment saves this file to disk, which readonly mode does not allow. Only images without local_path can be downloaded in readonly mode.");
10723
10768
  }
10724
- const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
10769
+ const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path, args.metadata_only === true);
10770
+ if (args.metadata_only) {
10771
+ return {
10772
+ content: [
10773
+ {
10774
+ type: "text",
10775
+ text: JSON.stringify({
10776
+ filename: result.filename,
10777
+ mimeType: result.mimeType,
10778
+ byte_length: result.byteLength ?? result.buffer.byteLength,
10779
+ metadata_only: true,
10780
+ }),
10781
+ },
10782
+ ],
10783
+ };
10784
+ }
10725
10785
  if (result.mimeType && !args.local_path) {
10726
10786
  // Return image inline as base64 so the AI can see it
10727
10787
  const base64 = result.buffer.toString("base64");
package/build/schemas.js CHANGED
@@ -3522,6 +3522,10 @@ export const DownloadAttachmentSchema = z.object({
3522
3522
  .string()
3523
3523
  .optional()
3524
3524
  .describe("Subdirectory of the download directory to save the file in (optional, defaults to the download directory itself). Names starting with a dot are rejected"),
3525
+ metadata_only: z
3526
+ .boolean()
3527
+ .optional()
3528
+ .describe("If true, return filename, mime type, and byte length without inline image bytes or a saved file. Default false keeps the existing download behavior."),
3525
3529
  });
3526
3530
  export const DownloadAttachmentRemoteSchema = z.object({
3527
3531
  project_id: z.string().describe("Project ID or URL-encoded path of the project"),
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ export const HELP_TOPICS = [
3
+ "overview",
4
+ "code-review",
5
+ "merge-requests",
6
+ "issues",
7
+ "pipelines",
8
+ "search",
9
+ "token-saving",
10
+ ];
11
+ export const HelpInputSchema = z.object({
12
+ topic: z
13
+ .enum(HELP_TOPICS)
14
+ .optional()
15
+ .describe("Guide to return. Omit for the overview and the list of topics. " +
16
+ "Values: overview, code-review, merge-requests, issues, pipelines, search, token-saving."),
17
+ });
18
+ export function isHelpTopic(value) {
19
+ for (const topic of HELP_TOPICS) {
20
+ if (topic === value) {
21
+ return true;
22
+ }
23
+ }
24
+ return false;
25
+ }
26
+ function assertNever(value) {
27
+ throw new Error(`Unhandled help topic: ${String(value)}`);
28
+ }
29
+ const GUIDES = {
30
+ overview: [
31
+ "GitLab MCP workflow guide. Default tools/list is 38 core tools plus discover_tools and help (40).",
32
+ "Call help with topic: code-review, merge-requests, issues, pipelines, search, token-saving.",
33
+ "Activate more categories with discover_tools. api is explicit-only (GITLAB_TOOLSETS=api).",
34
+ "project_id is a numeric ID or URL-encoded path (group%2Fproject). MR !123 → merge_request_iid=123.",
35
+ ].join("\n"),
36
+ "code-review": [
37
+ "Code review (2-step diffs):",
38
+ "1. list_merge_request_changed_files — paths only, no diff text.",
39
+ "2. get_merge_request_file_diff — 3-5 files per call.",
40
+ "3. create_merge_request_thread or create_draft_note, then bulk_publish_draft_notes.",
41
+ "Do not dump get_merge_request_diffs for a whole MR. Prefer jmespath on large results.",
42
+ ].join("\n"),
43
+ "merge-requests": [
44
+ "MR lifecycle: create_merge_request → review → approve_merge_request → merge_merge_request.",
45
+ "Lookup: project_id plus merge_request_iid (or source_branch).",
46
+ "Inspect: get_merge_request, get_merge_request_approval_state, get_merge_request_conflicts.",
47
+ "Discussions: mr_discussions, create_merge_request_thread, resolve_merge_request_thread.",
48
+ "merge_merge_request needs merge permission and often sha from get_merge_request.",
49
+ ].join("\n"),
50
+ issues: [
51
+ "Issues: list_issues (default scope is issues you created; use scope=all), my_issues, get_issue.",
52
+ "Write: create_issue, update_issue, create_issue_note, list_issue_discussions.",
53
+ "Small description edits: update_issue_description_patch instead of resending the full body.",
54
+ "project_id is a numeric ID or URL-encoded path.",
55
+ ].join("\n"),
56
+ pipelines: [
57
+ "Pipelines: discover_tools with category pipelines if those tools are hidden.",
58
+ "list_pipelines / get_pipeline, then list_pipeline_jobs / get_pipeline_job.",
59
+ "Logs: get_pipeline_job_output with pagination (limit / offset). Do not pull a whole trace.",
60
+ "Create/retry/cancel: create_pipeline, retry_pipeline, cancel_pipeline (destructive).",
61
+ ].join("\n"),
62
+ search: [
63
+ "search_repositories for projects. Code search: search_code, search_project_code, search_group_code",
64
+ "(needs GitLab advanced search or Zoekt; enable the search toolset via discover_tools).",
65
+ "If you already have an id or path, use get_project / get_issue / get_merge_request instead.",
66
+ ].join("\n"),
67
+ "token-saving": [
68
+ "Do not dump get_merge_request_diffs for a whole MR. Use list_merge_request_changed_files",
69
+ "then batched get_merge_request_file_diff.",
70
+ "Pass jmespath on any tool to shrink JSON. GITLAB_MCP_COMPACT_RESULTS replaces oversized",
71
+ "MCP replies with a preview plus a CLI command. Human CLI is never compacted.",
72
+ "Paginate logs (get_pipeline_job_output) and list endpoints. Prefer typed tools over",
73
+ "execute_graphql. Do not enable gitlab_api_request unless you accept its bypass risk.",
74
+ ].join("\n"),
75
+ };
76
+ export function getWorkflowGuide(topic = "overview") {
77
+ switch (topic) {
78
+ case "overview":
79
+ case "code-review":
80
+ case "merge-requests":
81
+ case "issues":
82
+ case "pipelines":
83
+ case "search":
84
+ case "token-saving":
85
+ return GUIDES[topic];
86
+ default:
87
+ return assertNever(topic);
88
+ }
89
+ }
90
+ export function readHelpTopic(value) {
91
+ if (value === undefined || value === null || value === "") {
92
+ return undefined;
93
+ }
94
+ if (typeof value === "string" && isHelpTopic(value)) {
95
+ return value;
96
+ }
97
+ return "invalid";
98
+ }
99
+ export function formatUnknownHelpTopic(value) {
100
+ return `Unknown help topic ${JSON.stringify(value)}. Available: ${HELP_TOPICS.join(", ")}`;
101
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * MCP initialize `instructions` — a short session briefing.
3
+ * Keep this under MCP_INSTRUCTIONS_MAX_BYTES so the handshake stays cheap.
4
+ */
5
+ export const MCP_INSTRUCTIONS_MAX_BYTES = 2048;
6
+ export const MCP_INSTRUCTIONS = [
7
+ "GitLab MCP. Default list: 38 core tools plus discover_tools and help (40). Call help for workflows.",
8
+ "MR review: list_merge_request_changed_files, then get_merge_request_file_diff for 3-5 files. Do not dump get_merge_request_diffs.",
9
+ "project_id: numeric ID or URL-encoded path (group%2Fproject). MR !123 uses merge_request_iid=123.",
10
+ "discover_tools: omit category to list; pass a category to activate for this session. api is explicit-only (GITLAB_TOOLSETS=api).",
11
+ "Shrink results: pass jmespath on any tool. Set GITLAB_MCP_COMPACT_RESULTS for oversized replies. Paginate get_pipeline_job_output.",
12
+ "Prefer typed tools over execute_graphql. Do not enable gitlab_api_request unless you accept allowlist and safety-check bypass. Prompt injection can drive it.",
13
+ "Destructive tools change GitLab state. Use the CLI (zereight-mcp-gitlab) for full diffs, logs, and artifacts.",
14
+ ].join("\n");
@@ -1,3 +1,4 @@
1
+ import { appendRecoveryHint } from "../utils/error-hints.js";
1
2
  /**
2
3
  * Same skips `discover_tools` uses when it adds tools: permission mode, regex
3
4
  * denial, hidden policy, and the slim profile. Those tools never join the list.
@@ -22,15 +23,15 @@ export function buildUnavailableToolMessage(input) {
22
23
  const base = `Tool "${input.toolName}" is not available on this server`;
23
24
  const toolset = input.toolset;
24
25
  if (toolset === undefined || input.deniedByRegex) {
25
- return base;
26
+ return appendRecoveryHint(base);
26
27
  }
27
28
  if (input.excludedBySlimProfile) {
28
- return `${base}. ${slimExclusionGuidance(input.toolName, toolset, input.enabledByCurrentToolsets)}`;
29
+ return appendRecoveryHint(`${base}. ${slimExclusionGuidance(input.toolName, toolset, input.enabledByCurrentToolsets)}`);
29
30
  }
30
31
  if (input.explicitToolsetOnly) {
31
- return `${base}. The "${toolset}" toolset is not enabled. discover_tools cannot activate it. Add "${toolset}" to GITLAB_TOOLSETS. GITLAB_TOOLSETS=all, GITLAB_TOOLS, and GITLAB_TOOL_PROFILE do not enable it.`;
32
+ return appendRecoveryHint(`${base}. The "${toolset}" toolset is not enabled. discover_tools cannot activate it. Add "${toolset}" to GITLAB_TOOLSETS. GITLAB_TOOLSETS=all, GITLAB_TOOLS, and GITLAB_TOOL_PROFILE do not enable it.`);
32
33
  }
33
- return `${base}. It belongs to the "${toolset}" toolset, which is not enabled: call discover_tools with category "${toolset}" or add "${toolset}" to GITLAB_TOOLSETS.`;
34
+ return appendRecoveryHint(`${base}. It belongs to the "${toolset}" toolset, which is not enabled: call discover_tools with category "${toolset}" or add "${toolset}" to GITLAB_TOOLSETS.`);
34
35
  }
35
36
  function slimExclusionGuidance(toolName, toolset, enabledByCurrentToolsets) {
36
37
  const viaToolsets = `add "${toolset}" to GITLAB_TOOLSETS (setting GITLAB_TOOLSETS disables the slim profile)`;
@@ -1,4 +1,6 @@
1
1
  import { isExcludedBySlimProfile } from "./tool-profile.js";
2
+ /** Meta tools re-added after every toolset filter so progressive disclosure stays available. */
3
+ export const ALWAYS_LISTED_META_TOOLS = ["discover_tools", "help"];
2
4
  export function selectExposedTools(input) {
3
5
  const additiveAllowed = input.isAdditiveEnablementAllowed ?? (() => true);
4
6
  const toolsAfterToolsets = input.tools.filter(tool => input.isInEnabledToolset(tool.name));
@@ -22,12 +24,16 @@ export function selectExposedTools(input) {
22
24
  ? toolsAfterPermission.filter(tool => !deniedToolsRegex.test(tool.name))
23
25
  : [...toolsAfterPermission];
24
26
  const filteredToolNames = new Set(toolsAfterRegex.map(tool => tool.name));
25
- const discoverTool = input.tools.find(tool => tool.name === "discover_tools");
26
- if (discoverTool && !filteredToolNames.has("discover_tools")) {
27
- const passesPermissionMode = input.isAllowedByPermissionMode("discover_tools");
28
- const passesRegex = deniedToolsRegex ? !deniedToolsRegex.test("discover_tools") : true;
27
+ for (const metaName of ALWAYS_LISTED_META_TOOLS) {
28
+ const metaTool = input.tools.find(tool => tool.name === metaName);
29
+ if (!metaTool || filteredToolNames.has(metaName)) {
30
+ continue;
31
+ }
32
+ const passesPermissionMode = input.isAllowedByPermissionMode(metaName);
33
+ const passesRegex = deniedToolsRegex ? !deniedToolsRegex.test(metaName) : true;
29
34
  if (passesPermissionMode && passesRegex) {
30
- toolsAfterRegex.push(discoverTool);
35
+ toolsAfterRegex.push(metaTool);
36
+ filteredToolNames.add(metaName);
31
37
  }
32
38
  }
33
39
  const withoutHidden = input.hiddenToolNames.size > 0
@@ -1,3 +1,4 @@
1
+ import { LIST_SUMMARY_ARGUMENT, LIST_SUMMARY_ARGUMENT_DESCRIPTION, toolAcceptsDecoratedSummary, } from "../utils/list-summary.js";
1
2
  const CONFIRMED_ARGUMENT = "_confirmed";
2
3
  const CONFIRMED_ARGUMENT_DESCRIPTION = "Set to true to confirm execution of this approval-required tool.";
3
4
  function isRecord(value) {
@@ -36,11 +37,29 @@ function withJmespathArgument(schema, context) {
36
37
  };
37
38
  return { ...schema, properties };
38
39
  }
40
+ function withSummaryArgument(schema, toolName) {
41
+ if (!toolAcceptsDecoratedSummary(toolName) || !isRecord(schema.properties)) {
42
+ return schema;
43
+ }
44
+ if (LIST_SUMMARY_ARGUMENT in schema.properties) {
45
+ return schema;
46
+ }
47
+ return {
48
+ ...schema,
49
+ properties: {
50
+ ...schema.properties,
51
+ [LIST_SUMMARY_ARGUMENT]: {
52
+ type: "boolean",
53
+ description: LIST_SUMMARY_ARGUMENT_DESCRIPTION,
54
+ },
55
+ },
56
+ };
57
+ }
39
58
  function decorateInputSchema(schema, toolName, context) {
40
59
  if (!isRecord(schema)) {
41
60
  return undefined;
42
61
  }
43
- return withJmespathArgument(withConfirmedArgument(cloneSchema(schema), toolName, context), context);
62
+ return withSummaryArgument(withJmespathArgument(withConfirmedArgument(cloneSchema(schema), toolName, context), context), toolName);
44
63
  }
45
64
  export function decorateToolForList(tool, context) {
46
65
  const inputSchema = decorateInputSchema(tool.inputSchema, tool.name, context);
@@ -3,6 +3,7 @@ import { USE_GITLAB_WIKI, USE_MILESTONE, USE_PIPELINE, SSE, STREAMABLE_HTTP } fr
3
3
  import { getToolDescription } from "./tool-descriptions.js";
4
4
  import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, ErasePipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateGroupMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWebhookSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UpdateProtectedBranchSchema, ProtectTagSchema, UnprotectBranchSchema, UnprotectTagSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupMilestoneSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWebhookSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadAttachmentRemoteSchema, DownloadJobArtifactsSchema, DownloadJobArtifactsRemoteSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, EditGroupMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, GetBranchSchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GetDeploymentSchema, CreateDeploymentSchema, UpdateDeploymentSchema, DeploymentApprovalSchema, ListDeploymentMergeRequestsSchema, GetDraftNoteSchema, GetEnvironmentSchema, UpdateEnvironmentSchema, StopEnvironmentSchema, StopStaleEnvironmentsSchema, DeleteReviewAppEnvironmentsSchema, PipelineTriggerIdSchema, ListPipelineTriggersSchema, CreatePipelineTriggerSchema, UpdatePipelineTriggerSchema, TriggerPipelineSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestDiscussionSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetGroupMilestoneSchema, GetGroupMilestoneIssuesSchema, GetGroupMilestoneMergeRequestsSchema, GetGroupMilestoneBurndownEventsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, PipelineJobControlSchema, GetPipelineSchema, GetRunnerSchema, GetPipelineVariablesSchema, UpdatePipelineMetadataSchema, DeletePipelineSchema, PipelineReportSchema, WaitForPipelineSchema, WaitForPipelineJobSchema, PlayPipelineJobsSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetProtectedTagSchema, GetTimelineEventsSchema, GetUsersSchema, GetUserSchema, WhoAmISchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListBranchesSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupMilestonesSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListPipelinesSchema, ListPipelineSchedulesSchema, GetPipelineScheduleSchema, ListPipelineSchedulePipelinesSchema, CreatePipelineScheduleSchema, UpdatePipelineScheduleSchema, DeletePipelineScheduleSchema, PlayPipelineScheduleSchema, TakeOwnershipPipelineScheduleSchema, GetPipelineScheduleVariableSchema, CreatePipelineScheduleVariableSchema, UpdatePipelineScheduleVariableSchema, DeletePipelineScheduleVariableSchema, ListGroupMembersSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListProjectRunnersSchema, ListReleasesSchema, ListTagsSchema, ListProtectedTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, ListSnippetsSchema, GetSnippetSchema, CreateSnippetSchema, UpdateSnippetSchema, DeleteSnippetSchema, UnapproveMergeRequestSchema, RebaseMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWebhookSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, } from "../schemas.js";
5
5
  import { GitLabApiRequestSchema } from "./gitlab-api-request.js";
6
+ import { HelpInputSchema } from "../server/mcp-help.js";
6
7
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
7
8
  // Define all available tools
8
9
  export const allTools = [
@@ -1427,10 +1428,16 @@ export const allTools = [
1427
1428
  },
1428
1429
  },
1429
1430
  },
1431
+ {
1432
+ name: "help",
1433
+ description: "Return a short GitLab workflow guide. Omit topic for the overview and topic list.",
1434
+ inputSchema: toJSONSchema(HelpInputSchema),
1435
+ },
1430
1436
  ];
1431
1437
  // Define which tools are read-only
1432
1438
  export const readOnlyTools = new Set([
1433
1439
  "discover_tools",
1440
+ "help",
1434
1441
  "health_check",
1435
1442
  "search_repositories",
1436
1443
  "search_code",
@@ -39,6 +39,7 @@ const TOOL_GUIDANCE = {
39
39
  download_attachment: "Retrieve a project attachment. Remote HTTP returns inline base64 or a URL; local stdio saves under GITLAB_DOWNLOAD_DIR and never overwrites.",
40
40
  gitlab_api_request: "Send one GitLab REST call under /api/v4. Readonly allows GET only; modify rejects DELETE. Enable only by listing api in GITLAB_TOOLSETS; all, discover_tools, and GITLAB_TOOLS do not. Prompt injection can drive this tool, so operators who rely on group, project, or tool restrictions must not enable it. With GITLAB_ALLOWED_PROJECT_IDS or a session project scope, only /projects/:id paths for an allowed project are accepted; body and query parameters that name another project are not checked.",
41
41
  execute_graphql: "Use only when no typed REST tool covers the operation. The query is sent directly and may include mutations.",
42
+ help: "Use this for a short workflow guide (code-review, pipelines, issues, token-saving). Omit topic for the overview. Detailed GitLab operations stay on the typed tools.",
42
43
  whoami: "Requires classic read_user or fine-grained User:Read. Use `get_user` or `get_users` to look up another user.",
43
44
  list_project_members: "Search project members by name or username. Use `list_group_members` for a group.",
44
45
  get_commit_diff: "Return the file diffs for one commit. Use `get_commit` for metadata without the patch.",