@zereight/mcp-gitlab 2.3.8-next.144 → 2.3.8-next.146

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
@@ -10,7 +10,7 @@
10
10
 
11
11
  `@zereight/mcp-gitlab`의 권장 로컬 PAT 설치입니다. 명령 인자는 [Claude Code](./docs/clients/claude-code.md)와 [Codex](./docs/clients/codex.md) 가이드와 같습니다.
12
12
 
13
- `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`.
13
+ `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
14
 
15
15
  토큰을 화면에 표시하지 않고 읽은 다음 export 하세요. 셸 기록에는 변수 이름만 남습니다.
16
16
 
@@ -202,7 +202,7 @@ CLI 인자는 환경 변수보다 우선합니다.
202
202
 
203
203
  `zereight-mcp-gitlab auth`는 MCP 서버 플래그가 아니라 서브커맨드입니다. GitLab device flow를 실행한 뒤 종료합니다. [CLI 인자](./docs/getting-started/cli-arguments.md#auth)를 참고하세요.
204
204
 
205
- 같은 바이너리는 `gh` 스타일 GitLab CLI이기도 합니다 (`tool <name>` 또는 `mr list` 같은 curated 명령). **MCP 등록은 필요 없습니다** — PAT를 설정하거나, `auth`를 실행한 뒤 `GITLAB_USE_OAUTH=true`를 설정하면 됩니다: [MCP 없이 CLI 사용하기](./docs/getting-started/cli-without-mcp.md). 전체 페이로드는 터미널에서 이 CLI로 읽습니다. MCP 응답은 대화에 남습니다. `GITLAB_MCP_COMPACT_RESULTS=true`(또는 `--compact-results`)를 켜면 큰 도구 응답이 미리보기와 CLI 명령으로 바뀝니다. Human CLI 자체는 compact 되지 않습니다. permission mode, toolsets, denied-tools regex, tool-policy는 MCP와 같습니다. destructive 도구와 `GITLAB_TOOL_POLICY_APPROVE` 도구는 `--yes`가 필요합니다. [Human CLI](./docs/getting-started/cli-arguments.md#human-cli)를 참고하세요.
205
+ 같은 바이너리는 `gh` 스타일 GitLab CLI이기도 합니다 (`tool <name>` 또는 `mr list` 같은 curated 명령). **MCP 등록은 필요 없습니다** — PAT를 설정하거나, `auth`를 실행한 뒤 `GITLAB_USE_OAUTH=true`를 설정하면 됩니다: [MCP 없이 CLI 사용하기](./docs/getting-started/cli-without-mcp.md). `setup --cursor` / `setup --claude`는 클라이언트 설정을 출력하고, `doctor`는 토큰·API URL·toolset·버전을 점검합니다. 전체 페이로드는 터미널에서 이 CLI로 읽습니다. MCP 응답은 대화에 남습니다. `GITLAB_MCP_COMPACT_RESULTS=true`(또는 `--compact-results`)를 켜면 큰 도구 응답이 미리보기와 CLI 명령으로 바뀝니다. Human CLI 자체는 compact 되지 않습니다. permission mode, toolsets, denied-tools regex, tool-policy는 MCP와 같습니다. destructive 도구와 `GITLAB_TOOL_POLICY_APPROVE` 도구는 `--yes`가 필요합니다. [Human CLI](./docs/getting-started/cli-arguments.md#human-cli)를 참고하세요.
206
206
 
207
207
  > **세밀한 도구 필터링:** `GITLAB_PERMISSION_MODE=modify`로 생성/수정은 허용하고 모든 삭제 도구와
208
208
  > 파괴적인 중단(teardown) 도구(`cancel_pipeline`, `cancel_pipeline_job`, `stop_environment`,
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  Install `@zereight/mcp-gitlab` with the recommended local personal-access-token setup. These commands use the same arguments as the [Claude Code](./docs/clients/claude-code.md) and [Codex](./docs/clients/codex.md) guides.
17
17
 
18
- Install the `zereight-mcp-gitlab` binary first with `npm install -g @zereight/mcp-gitlab`, or Homebrew `brew tap zereight/gitlab-mcp https://github.com/zereight/gitlab-mcp` and `brew install zereight/gitlab-mcp/zereight-mcp-gitlab`.
18
+ Install the `zereight-mcp-gitlab` binary first with `npm install -g @zereight/mcp-gitlab`, or Homebrew `brew tap zereight/gitlab-mcp https://github.com/zereight/gitlab-mcp` and `brew install zereight/gitlab-mcp/zereight-mcp-gitlab`. Official MCP Registry listing: [`io.github.zereight/gitlab-mcp`](https://registry.modelcontextprotocol.io/?q=io.github.zereight%2Fgitlab-mcp). One-liner: `npx -y @zereight/mcp-gitlab@latest`.
19
19
 
20
20
  Read the token without echo, then export it. The shell history keeps the variable name.
21
21
 
@@ -207,7 +207,7 @@ CLI arguments take precedence over environment variables.
207
207
 
208
208
  `zereight-mcp-gitlab auth` is a subcommand (not an MCP server flag). It runs GitLab device flow and exits. See [CLI Arguments](./docs/getting-started/cli-arguments.md#auth).
209
209
 
210
- The same binary is also a `gh`-style GitLab CLI (`tool <name>` or curated forms such as `mr list`). **No MCP registration is needed** — set a PAT, or run `auth` and set `GITLAB_USE_OAUTH=true`: see [CLI without MCP](./docs/getting-started/cli-without-mcp.md). Use it for the full payload in a terminal. MCP replies stay in the chat unless you set `GITLAB_MCP_COMPACT_RESULTS=true` (or `--compact-results`), which replaces oversized tool replies with a preview and a CLI command. Human CLI is never compacted. Same permission mode, toolsets, denied-tools regex, and tool-policy filters. Destructive tools and `GITLAB_TOOL_POLICY_APPROVE` tools need `--yes`. See [Human CLI](./docs/getting-started/cli-arguments.md#human-cli).
210
+ The same binary is also a `gh`-style GitLab CLI (`tool <name>` or curated forms such as `mr list`). **No MCP registration is needed** — set a PAT, or run `auth` and set `GITLAB_USE_OAUTH=true`: see [CLI without MCP](./docs/getting-started/cli-without-mcp.md). `setup --cursor` / `setup --claude` prints client config; `doctor` checks token, API URL, toolsets, and version. Use it for the full payload in a terminal. MCP replies stay in the chat unless you set `GITLAB_MCP_COMPACT_RESULTS=true` (or `--compact-results`), which replaces oversized tool replies with a preview and a CLI command. Human CLI is never compacted. Same permission mode, toolsets, denied-tools regex, and tool-policy filters. Destructive tools and `GITLAB_TOOL_POLICY_APPROVE` tools need `--yes`. See [Human CLI](./docs/getting-started/cli-arguments.md#human-cli).
211
211
 
212
212
  > **Fine-grained tool filtering:** use `GITLAB_PERMISSION_MODE=modify` to allow create/update while
213
213
  > blocking every delete tool and the destructive teardown tools (`cancel_pipeline`,
package/README.pt-BR.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  Instalação local recomendada com PAT para `@zereight/mcp-gitlab`. Os argumentos são os mesmos dos guias de [Claude Code](./docs/clients/claude-code.md) e [Codex](./docs/clients/codex.md).
12
12
 
13
- Instale o binário `zereight-mcp-gitlab` antes. `npm install -g @zereight/mcp-gitlab`, ou Homebrew `brew tap zereight/gitlab-mcp https://github.com/zereight/gitlab-mcp` e `brew install zereight/gitlab-mcp/zereight-mcp-gitlab`.
13
+ Instale o binário `zereight-mcp-gitlab` antes. `npm install -g @zereight/mcp-gitlab`, ou Homebrew `brew tap zereight/gitlab-mcp https://github.com/zereight/gitlab-mcp` e `brew install zereight/gitlab-mcp/zereight-mcp-gitlab`. Listagem oficial no MCP Registry: [`io.github.zereight/gitlab-mcp`](https://registry.modelcontextprotocol.io/?q=io.github.zereight%2Fgitlab-mcp). Instalação em uma linha: `npx -y @zereight/mcp-gitlab@latest`.
14
14
 
15
15
  Leia o token sem eco e exporte. O histórico do shell fica só com o nome da variável.
16
16
 
@@ -202,7 +202,7 @@ Os argumentos de CLI têm precedência sobre as variáveis de ambiente.
202
202
 
203
203
  `zereight-mcp-gitlab auth` é um subcomando, não uma flag do servidor MCP. Ele executa o device flow do GitLab e encerra. Veja [Argumentos de CLI](./docs/getting-started/cli-arguments.md#auth).
204
204
 
205
- O mesmo binário também é uma CLI do GitLab no estilo `gh` (`tool <name>` ou formas curadas como `mr list`). **Não é necessário registrar o MCP** — defina um PAT, ou execute `auth` e defina `GITLAB_USE_OAUTH=true`: veja [CLI sem MCP](./docs/getting-started/cli-without-mcp.md). Use-o para ver a carga completa no terminal. As respostas do MCP permanecem no chat, a menos que você defina `GITLAB_MCP_COMPACT_RESULTS=true` (ou `--compact-results`), o que substitui respostas grandes de ferramentas por uma prévia e um comando CLI. A CLI humana nunca é compactada. Os mesmos filtros de permission mode, toolsets, denied-tools regex e tool-policy se aplicam. Ferramentas destrutivas e ferramentas com `GITLAB_TOOL_POLICY_APPROVE` exigem `--yes`. Consulte [CLI Humana](./docs/getting-started/cli-arguments.md#human-cli).
205
+ O mesmo binário também é uma CLI do GitLab no estilo `gh` (`tool <name>` ou formas curadas como `mr list`). **Não é necessário registrar o MCP** — defina um PAT, ou execute `auth` e defina `GITLAB_USE_OAUTH=true`: veja [CLI sem MCP](./docs/getting-started/cli-without-mcp.md). `setup --cursor` / `setup --claude` imprime a configuração do cliente; `doctor` verifica token, URL da API, toolsets e versão. Use-o para ver a carga completa no terminal. As respostas do MCP permanecem no chat, a menos que você defina `GITLAB_MCP_COMPACT_RESULTS=true` (ou `--compact-results`), o que substitui respostas grandes de ferramentas por uma prévia e um comando CLI. A CLI humana nunca é compactada. Os mesmos filtros de permission mode, toolsets, denied-tools regex e tool-policy se aplicam. Ferramentas destrutivas e ferramentas com `GITLAB_TOOL_POLICY_APPROVE` exigem `--yes`. Consulte [CLI Humana](./docs/getting-started/cli-arguments.md#human-cli).
206
206
 
207
207
  > **Filtragem granular de ferramentas:** use `GITLAB_PERMISSION_MODE=modify` para permitir criação/atualização enquanto
208
208
  > bloqueia todas as ferramentas de exclusão e as ferramentas destrutivas de teardown (`cancel_pipeline`,
package/README.zh-CN.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  下面是 `@zereight/mcp-gitlab` 的推荐本地 PAT 安装。命令参数与 [Claude Code](./docs/clients/claude-code.md) 和 [Codex](./docs/clients/codex.md) 指南相同。
12
12
 
13
- 先安装 `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`。
13
+ 先安装 `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
14
 
15
15
  先不回显地读取令牌,再 export。shell 历史里只保留变量名。
16
16
 
@@ -202,7 +202,7 @@ CLI 参数优先于环境变量。
202
202
 
203
203
  `zereight-mcp-gitlab auth` 是子命令,不是 MCP 服务器参数。它运行 GitLab device flow 后退出。参见 [CLI 参数](./docs/getting-started/cli-arguments.md#auth)。
204
204
 
205
- 同一二进制也是 `gh` 风格的 GitLab CLI(`tool <name>`,或 `mr list` 这类 curated 命令)。**无需注册 MCP**——设置 PAT,或运行 `auth` 后设置 `GITLAB_USE_OAUTH=true`:参见 [无需 MCP 使用 CLI](./docs/getting-started/cli-without-mcp.md)。完整结果在终端用 CLI 查看。MCP 回复仍进入对话。设置 `GITLAB_MCP_COMPACT_RESULTS=true`(或 `--compact-results`)后,过大的工具回复会变成预览加上 CLI 命令。Human CLI 本身不会被 compact。permission mode、toolsets、denied-tools regex 和 tool-policy 与 MCP 相同。破坏性工具以及 `GITLAB_TOOL_POLICY_APPROVE` 工具需要 `--yes`。参见 [Human CLI](./docs/getting-started/cli-arguments.md#human-cli)。
205
+ 同一二进制也是 `gh` 风格的 GitLab CLI(`tool <name>`,或 `mr list` 这类 curated 命令)。**无需注册 MCP**——设置 PAT,或运行 `auth` 后设置 `GITLAB_USE_OAUTH=true`:参见 [无需 MCP 使用 CLI](./docs/getting-started/cli-without-mcp.md)。`setup --cursor` / `setup --claude` 会打印客户端配置;`doctor` 检查 token、API URL、toolset 和版本。完整结果在终端用 CLI 查看。MCP 回复仍进入对话。设置 `GITLAB_MCP_COMPACT_RESULTS=true`(或 `--compact-results`)后,过大的工具回复会变成预览加上 CLI 命令。Human CLI 本身不会被 compact。permission mode、toolsets、denied-tools regex 和 tool-policy 与 MCP 相同。破坏性工具以及 `GITLAB_TOOL_POLICY_APPROVE` 工具需要 `--yes`。参见 [Human CLI](./docs/getting-started/cli-arguments.md#human-cli)。
206
206
 
207
207
  > **细粒度工具过滤:**使用 `GITLAB_PERMISSION_MODE=modify` 允许创建/更新,同时阻止所有删除工具以及
208
208
  > 破坏性的拆除(teardown)工具(`cancel_pipeline`、`cancel_pipeline_job`、`stop_environment`、
package/build/cli/args.js CHANGED
@@ -7,7 +7,17 @@ export class CliUsageError extends Error {
7
7
  }
8
8
  }
9
9
  const SCRIPT_PATH_PATTERN = /\.(cjs|mjs|js|cts|mts|ts)$/;
10
- const FLAGS_WITHOUT_VALUE = new Set(["help", "yes", ...BOOLEAN_CLI_FLAG_NAMES]);
10
+ const FLAGS_WITHOUT_VALUE = new Set([
11
+ "help",
12
+ "yes",
13
+ "cursor",
14
+ "claude",
15
+ "write",
16
+ "skill",
17
+ "probe",
18
+ "oauth",
19
+ ...BOOLEAN_CLI_FLAG_NAMES,
20
+ ]);
11
21
  const GLOBAL_FLAG_NAMES = new Set([
12
22
  "help",
13
23
  "yes",
@@ -199,7 +199,11 @@ export function canonicalGroup(name) {
199
199
  return GROUP_ALIASES[name];
200
200
  }
201
201
  export function isKnownCliCommand(name) {
202
- return name === "auth" || name === "tool" || canonicalGroup(name) !== undefined;
202
+ return (name === "auth" ||
203
+ name === "tool" ||
204
+ name === "setup" ||
205
+ name === "doctor" ||
206
+ canonicalGroup(name) !== undefined);
203
207
  }
204
208
  export function findCuratedCommand(group, action) {
205
209
  const canonical = canonicalGroup(group);
@@ -0,0 +1,104 @@
1
+ import { SERVER_VERSION } from "../server/version.js";
2
+ export function doctorHelpText() {
3
+ return `zereight-mcp-gitlab doctor
4
+
5
+ Check token presence, API URL, permission mode, toolsets, and package version.
6
+ Does not print secret values.
7
+
8
+ Usage:
9
+ zereight-mcp-gitlab doctor [--probe]
10
+ zereight-mcp-gitlab doctor --help
11
+
12
+ Flags:
13
+ --probe GET {apiUrl}/user with the configured credentials (network)
14
+ `;
15
+ }
16
+ export function packageVersion() {
17
+ return SERVER_VERSION;
18
+ }
19
+ export function runDoctor(input) {
20
+ const checks = [
21
+ credentialCheck(input.credentials),
22
+ apiUrlCheck(input.apiUrl),
23
+ permissionModeCheck(input.permissionMode),
24
+ toolsetCheck(input.toolsetsRaw),
25
+ versionCheck(input.version),
26
+ ];
27
+ return formatDoctorReport(checks);
28
+ }
29
+ export async function runDoctorWithOptionalProbe(input) {
30
+ const report = runDoctor(input);
31
+ if (input.probe === undefined) {
32
+ return report;
33
+ }
34
+ const probe = await input.probe(input.apiUrl);
35
+ return formatDoctorReport([
36
+ ...report.checks,
37
+ { name: "gitlab_user", ok: probe.ok, detail: probe.detail },
38
+ ]);
39
+ }
40
+ export function formatDoctorReport(checks) {
41
+ const ok = checks.every(check => check.ok);
42
+ const lines = checks.map(check => `${check.ok ? "ok" : "fail"} ${check.name}: ${check.detail}`);
43
+ return {
44
+ checks,
45
+ ok,
46
+ text: `${lines.join("\n")}\n`,
47
+ };
48
+ }
49
+ function credentialCheck(credentials) {
50
+ const hasAny = credentials.hasPersonalAccessToken ||
51
+ credentials.hasJobToken ||
52
+ credentials.hasCookie ||
53
+ credentials.useOAuth;
54
+ return {
55
+ name: "credentials",
56
+ ok: hasAny,
57
+ detail: hasAny
58
+ ? "a token, job token, cookie, or OAuth is configured (value hidden)"
59
+ : "set GITLAB_PERSONAL_ACCESS_TOKEN or run `zereight-mcp-gitlab auth`",
60
+ };
61
+ }
62
+ function apiUrlCheck(apiUrl) {
63
+ try {
64
+ const parsed = new URL(apiUrl);
65
+ const hasApi = parsed.pathname.includes("/api/");
66
+ return {
67
+ name: "api_url",
68
+ ok: hasApi,
69
+ detail: hasApi
70
+ ? `${parsed.origin}${parsed.pathname}`
71
+ : `${apiUrl} is missing /api/v4 (use the API URL, not the web root)`,
72
+ };
73
+ }
74
+ catch {
75
+ return {
76
+ name: "api_url",
77
+ ok: false,
78
+ detail: `${apiUrl} is not a valid URL`,
79
+ };
80
+ }
81
+ }
82
+ function permissionModeCheck(mode) {
83
+ const ok = mode === "readonly" || mode === "modify" || mode === "full";
84
+ return {
85
+ name: "permission_mode",
86
+ ok,
87
+ detail: ok ? mode : `${mode} is not readonly, modify, or full`,
88
+ };
89
+ }
90
+ function toolsetCheck(raw) {
91
+ const value = raw === undefined || raw.trim() === "" ? "core (default)" : raw.trim();
92
+ return {
93
+ name: "toolsets",
94
+ ok: true,
95
+ detail: value,
96
+ };
97
+ }
98
+ function versionCheck(version) {
99
+ return {
100
+ name: "version",
101
+ ok: version !== "unknown",
102
+ detail: version,
103
+ };
104
+ }
package/build/cli/help.js CHANGED
@@ -8,6 +8,8 @@ export function globalHelpText() {
8
8
 
9
9
  Usage:
10
10
  ${BINARY} auth [options]
11
+ ${BINARY} setup --cursor|--claude [options]
12
+ ${BINARY} doctor [--probe]
11
13
  ${BINARY} tool <tool-name> [options]
12
14
  ${BINARY} <group> <action> [options]
13
15
  ${BINARY} --help
@@ -5,6 +5,8 @@ import { applyFlagAliases, argvHasHelp, CliUsageError, parseArgv, parseToolArgs,
5
5
  import { liveCliExposure } from "./exposure.js";
6
6
  import { canonicalGroup, findCuratedCommand, isKnownCliCommand, resolveScopedTool, } from "./curated.js";
7
7
  import { curatedHelpText, globalHelpText, groupHelpText, toolHelpText, toolIndexHelpText, } from "./help.js";
8
+ import { doctorHelpText } from "./doctor.js";
9
+ import { readSetupOptions, setupHelpText } from "./setup.js";
8
10
  export function isCliInvocation(argv) {
9
11
  if (argvHasHelp(argv)) {
10
12
  return true;
@@ -38,6 +40,24 @@ function resolveCliUnsafe(argv, permissionMode, exposure) {
38
40
  if (head === "auth") {
39
41
  return { kind: "auth" };
40
42
  }
43
+ if (head === "setup") {
44
+ if (action !== undefined) {
45
+ return { kind: "usage", message: `Unexpected extra arguments: ${parsed.positionals.slice(1).join(" ")}` };
46
+ }
47
+ if (parsed.help) {
48
+ return { kind: "help", text: setupHelpText() };
49
+ }
50
+ return { kind: "setup", options: readSetupOptions(parsed.toolFlags) };
51
+ }
52
+ if (head === "doctor") {
53
+ if (action !== undefined) {
54
+ return { kind: "usage", message: `Unexpected extra arguments: ${parsed.positionals.slice(1).join(" ")}` };
55
+ }
56
+ if (parsed.help) {
57
+ return { kind: "help", text: doctorHelpText() };
58
+ }
59
+ return { kind: "doctor", probe: parsed.toolFlags.probe === "true" };
60
+ }
41
61
  if (head === "tool") {
42
62
  if (extra !== undefined) {
43
63
  return { kind: "usage", message: `Unexpected extra arguments: ${parsed.positionals.slice(2).join(" ")}` };
@@ -0,0 +1,137 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { CliUsageError } from "./args.js";
4
+ const DEFAULT_API_URL = "https://gitlab.com/api/v4";
5
+ const SKILL_INSTALL = "npx skills add zereight/gitlab-mcp --skill gitlab-mcp-skill";
6
+ export function setupHelpText() {
7
+ return `zereight-mcp-gitlab setup
8
+
9
+ Print (or write) a local MCP client config. Tokens stay in the environment.
10
+
11
+ Usage:
12
+ zereight-mcp-gitlab setup --cursor [--oauth] [--write] [--dir PATH]
13
+ zereight-mcp-gitlab setup --claude [--oauth] [--write]
14
+ zereight-mcp-gitlab setup --help
15
+
16
+ Flags:
17
+ --cursor Cursor mcp.json snippet (default path .cursor/mcp.json)
18
+ --claude Claude Code one-liner (claude mcp add)
19
+ --oauth Use GITLAB_USE_OAUTH instead of a PAT
20
+ --write Write the Cursor file or print the Claude command as the install step
21
+ --skill Also print the Agent Skill install command
22
+ --dir PATH Directory that contains .cursor/mcp.json (default: cwd)
23
+ --api-url GitLab API URL (default ${DEFAULT_API_URL})
24
+
25
+ Dry-run is the default. --write is required to create or merge .cursor/mcp.json.
26
+ `;
27
+ }
28
+ export function readSetupOptions(flags, directory = process.cwd()) {
29
+ const cursor = flags.cursor === "true";
30
+ const claude = flags.claude === "true";
31
+ if (cursor === claude) {
32
+ throw new CliUsageError("setup requires exactly one of --cursor or --claude");
33
+ }
34
+ return {
35
+ client: cursor ? "cursor" : "claude",
36
+ oauth: flags.oauth === "true" || flags.use_oauth === "true",
37
+ write: flags.write === "true",
38
+ skill: flags.skill === "true",
39
+ directory: flags.dir ?? directory,
40
+ apiUrl: flags.api_url ?? DEFAULT_API_URL,
41
+ };
42
+ }
43
+ export function cursorMcpConfig(options) {
44
+ const env = {
45
+ GITLAB_API_URL: options.apiUrl,
46
+ };
47
+ if (options.oauth) {
48
+ env.GITLAB_USE_OAUTH = "true";
49
+ }
50
+ else {
51
+ env.GITLAB_PERSONAL_ACCESS_TOKEN = "${env:GITLAB_PERSONAL_ACCESS_TOKEN}";
52
+ }
53
+ return {
54
+ mcpServers: {
55
+ gitlab: {
56
+ command: "zereight-mcp-gitlab",
57
+ env,
58
+ },
59
+ },
60
+ };
61
+ }
62
+ export function claudeMcpAddCommand(options) {
63
+ const envFlags = options.oauth
64
+ ? `--env GITLAB_USE_OAUTH=true --env GITLAB_API_URL=${options.apiUrl}`
65
+ : `--env GITLAB_PERSONAL_ACCESS_TOKEN="$GITLAB_PERSONAL_ACCESS_TOKEN" --env GITLAB_API_URL=${options.apiUrl}`;
66
+ return `claude mcp add gitlab --transport stdio --scope local ${envFlags} -- zereight-mcp-gitlab`;
67
+ }
68
+ export function runSetup(options) {
69
+ switch (options.client) {
70
+ case "cursor":
71
+ return runCursorSetup(options);
72
+ case "claude":
73
+ return runClaudeSetup(options);
74
+ default: {
75
+ const exhaustive = options.client;
76
+ throw new Error(`Unhandled setup client: ${String(exhaustive)}`);
77
+ }
78
+ }
79
+ }
80
+ function runCursorSetup(options) {
81
+ const snippet = cursorMcpConfig(options);
82
+ const target = join(options.directory, ".cursor", "mcp.json");
83
+ const body = `${JSON.stringify(snippet, null, 2)}\n`;
84
+ const skillLine = options.skill ? `\nSkill: ${SKILL_INSTALL}\n` : "";
85
+ if (!options.write) {
86
+ return {
87
+ text: `Dry-run Cursor config (pass --write to merge ${target}):\n${body}` + skillLine,
88
+ };
89
+ }
90
+ const merged = mergeMcpJson(readJsonFile(target), snippet);
91
+ mkdirSync(dirname(target), { recursive: true });
92
+ writeFileSync(target, `${JSON.stringify(merged, null, 2)}\n`);
93
+ return {
94
+ text: `Wrote ${target}\n${skillLine}`,
95
+ writtenPath: target,
96
+ };
97
+ }
98
+ function runClaudeSetup(options) {
99
+ const command = claudeMcpAddCommand(options);
100
+ const skillLine = options.skill ? `\nSkill: ${SKILL_INSTALL}\n` : "";
101
+ return {
102
+ text: `${command}\n${skillLine}`,
103
+ };
104
+ }
105
+ function readJsonFile(path) {
106
+ if (!existsSync(path)) {
107
+ return {};
108
+ }
109
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
110
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
111
+ throw new CliUsageError(`${path} is not a JSON object`);
112
+ }
113
+ const copy = {};
114
+ for (const [key, value] of Object.entries(parsed)) {
115
+ copy[key] = value;
116
+ }
117
+ return copy;
118
+ }
119
+ function mergeMcpJson(existing, snippet) {
120
+ const existingServers = typeof existing.mcpServers === "object" &&
121
+ existing.mcpServers !== null &&
122
+ !Array.isArray(existing.mcpServers)
123
+ ? existing.mcpServers
124
+ : {};
125
+ const snippetServers = typeof snippet.mcpServers === "object" &&
126
+ snippet.mcpServers !== null &&
127
+ !Array.isArray(snippet.mcpServers)
128
+ ? snippet.mcpServers
129
+ : {};
130
+ return {
131
+ ...existing,
132
+ mcpServers: {
133
+ ...existingServers,
134
+ ...snippetServers,
135
+ },
136
+ };
137
+ }
package/build/index.js CHANGED
@@ -77,6 +77,8 @@ import { createGitLabOAuthClient, ensureOAuthToken } from "./oauth.js";
77
77
  import { getPositionalCliCommand } from "./cli-command.js";
78
78
  import { runAuthCommandAsync } from "./auth-cli.js";
79
79
  import { isCliInvocation, resolveCli } from "./cli/router.js";
80
+ import { runDoctorWithOptionalProbe, packageVersion, } from "./cli/doctor.js";
81
+ import { runSetup } from "./cli/setup.js";
80
82
  import { formatCliError, formatToolOutput } from "./cli/output.js";
81
83
  import { compactMcpToolResult, readOwnRecord } from "./cli/compact-mcp-result.js";
82
84
  import { compileDeniedToolsRegex } from "./tools/denied-regex.js";
@@ -112,6 +114,8 @@ import { parseGitLabVersionApiResponse, } from "./utils/gitlab-instance-version.
112
114
  import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
113
115
  import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
114
116
  import { applyJmespathToToolResult, getJmespathSyntaxError, JMESPATH_TOOL_ARGUMENT, JMESPATH_TOOL_ARGUMENT_DESCRIPTION, omitJmespathArgument, readJmespathExpression, } from "./utils/jmespath-tool-result.js";
117
+ import { appendRecoveryHint } from "./utils/error-hints.js";
118
+ import { applyListSummaryToToolResult, omitListSummaryArgument, readListSummaryRequested, toolAcceptsDecoratedSummary, } from "./utils/list-summary.js";
115
119
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
116
120
  import { zodSchemaFor } from "./utils/schema.js";
117
121
  import { GitLabClientPool } from "./gitlab-client-pool.js";
@@ -227,14 +231,18 @@ function createServer() {
227
231
  const toolName = request.params.name;
228
232
  const start = Date.now();
229
233
  let maskingEngine;
230
- const jmespathExpression = readJmespathExpression(request.params.arguments);
234
+ const requestArguments = readOwnRecord(request.params.arguments);
235
+ const jmespathExpression = readJmespathExpression(requestArguments === undefined ? undefined : { ...requestArguments });
236
+ const listSummaryRequested = toolAcceptsDecoratedSummary(toolName) &&
237
+ readListSummaryRequested(requestArguments === undefined ? undefined : { ...requestArguments });
231
238
  const logCompletion = (result, completionOptions) => {
232
239
  const durationMs = Date.now() - start;
233
240
  logger.info({ tool: toolName, event: "tool_call_done", durationMs }, `tool_call_done: ${toolName} (${durationMs}ms)`);
234
241
  const isPlainTextResult = toolName === "get_pipeline_job_output" ||
235
242
  toolName === "get_job_artifact_file" ||
236
243
  (toolName === "download_release_asset" && !IS_REMOTE);
237
- const filtered = applyJmespathToToolResult(redactErrorResultUrls(result), jmespathExpression, {
244
+ const summarized = applyListSummaryToToolResult(redactErrorResultUrls(result), listSummaryRequested);
245
+ const filtered = applyJmespathToToolResult(summarized, jmespathExpression, {
238
246
  ...completionOptions,
239
247
  maskValue: maskingEngine
240
248
  ? (value) => maskingEngine.maskValue(value)
@@ -475,8 +483,13 @@ function createServer() {
475
483
  const { _confirmed, ...cleanArgs } = request.params.arguments || {};
476
484
  request.params.arguments = cleanArgs;
477
485
  }
478
- if (request.params.arguments && typeof request.params.arguments === "object") {
479
- request.params.arguments = omitJmespathArgument(request.params.arguments);
486
+ const ownedArguments = readOwnRecord(request.params.arguments);
487
+ if (ownedArguments) {
488
+ let cleanedArguments = omitJmespathArgument({ ...ownedArguments });
489
+ if (toolAcceptsDecoratedSummary(toolName)) {
490
+ cleanedArguments = omitListSummaryArgument(cleanedArguments);
491
+ }
492
+ request.params.arguments = cleanedArguments;
480
493
  }
481
494
  if (sessionContext) {
482
495
  // Policy selection and the tool handler must use the same request-local
@@ -498,7 +511,7 @@ function createServer() {
498
511
  content: [
499
512
  {
500
513
  type: "text",
501
- text: safeError instanceof Error ? safeError.message : String(safeError),
514
+ text: appendRecoveryHint(safeError instanceof Error ? safeError.message : String(safeError)),
502
515
  },
503
516
  ],
504
517
  isError: true,
@@ -1416,12 +1429,12 @@ async function handleGitLabError(response) {
1416
1429
  // before the common tool-call boundary returns/logs it.
1417
1430
  logger.error("GitLab API Rate Limit Exceeded");
1418
1431
  logger.error("User API Key Rate limit exceeded. Please try again later.");
1419
- const error = new GitLabApiError(`GitLab API Rate Limit Exceeded: ${errorBody}`);
1432
+ const error = new GitLabApiError(appendRecoveryHint(`GitLab API Rate Limit Exceeded: ${errorBody}`));
1420
1433
  throw error;
1421
1434
  }
1422
1435
  else {
1423
1436
  // Handle other API errors
1424
- const error = new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
1437
+ const error = new GitLabApiError(appendRecoveryHint(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`));
1425
1438
  throw error;
1426
1439
  }
1427
1440
  }
@@ -7644,7 +7657,7 @@ function getSafeAttachmentFilename(filename) {
7644
7657
  }
7645
7658
  return filename;
7646
7659
  }
7647
- async function downloadAttachment(projectId, secret, filename, localPath) {
7660
+ async function downloadAttachment(projectId, secret, filename, localPath, metadataOnly = false) {
7648
7661
  const effectiveProjectId = getEffectiveProjectId(projectId);
7649
7662
  const safeFilename = getSafeAttachmentFilename(filename);
7650
7663
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/uploads/${encodeGitLabPathSegment(secret)}/${encodeGitLabPathSegment(safeFilename)}`);
@@ -7653,6 +7666,18 @@ async function downloadAttachment(projectId, secret, filename, localPath) {
7653
7666
  await handleGitLabError(response);
7654
7667
  }
7655
7668
  const mimeType = getImageMimeType(safeFilename);
7669
+ if (metadataOnly) {
7670
+ const headerLength = Number(response.headers.get("content-length"));
7671
+ if (response.body) {
7672
+ await response.body.cancel();
7673
+ }
7674
+ return {
7675
+ buffer: Buffer.alloc(0),
7676
+ filename: safeFilename,
7677
+ mimeType,
7678
+ byteLength: Number.isFinite(headerLength) ? headerLength : undefined,
7679
+ };
7680
+ }
7656
7681
  // Non-image files, and images with local_path, are saved as new files under the
7657
7682
  // download directory. Upload names and local_path come from untrusted content, so
7658
7683
  // dot names (.git, .vscode, .envrc) are refused and existing files are never replaced.
@@ -10716,10 +10741,27 @@ async function handleToolCall(params) {
10716
10741
  };
10717
10742
  }
10718
10743
  // Readonly mode never writes to disk: images still come back inline.
10719
- if (GITLAB_PERMISSION_MODE === "readonly" && (!mimeType || args.local_path)) {
10744
+ if (GITLAB_PERMISSION_MODE === "readonly" &&
10745
+ !args.metadata_only &&
10746
+ (!mimeType || args.local_path)) {
10720
10747
  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.");
10721
10748
  }
10722
- const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
10749
+ const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path, args.metadata_only === true);
10750
+ if (args.metadata_only) {
10751
+ return {
10752
+ content: [
10753
+ {
10754
+ type: "text",
10755
+ text: JSON.stringify({
10756
+ filename: result.filename,
10757
+ mimeType: result.mimeType,
10758
+ byte_length: result.byteLength ?? result.buffer.byteLength,
10759
+ metadata_only: true,
10760
+ }),
10761
+ },
10762
+ ],
10763
+ };
10764
+ }
10723
10765
  if (result.mimeType && !args.local_path) {
10724
10766
  // Return image inline as base64 so the AI can see it
10725
10767
  const base64 = result.buffer.toString("base64");
@@ -13005,6 +13047,43 @@ async function main() {
13005
13047
  process.stderr.write(`${cli.message}\n`);
13006
13048
  process.exit(2);
13007
13049
  }
13050
+ if (cli.kind === "setup") {
13051
+ try {
13052
+ const result = runSetup({
13053
+ ...cli.options,
13054
+ apiUrl: getConfig("api-url", "GITLAB_API_URL", cli.options.apiUrl),
13055
+ });
13056
+ process.stdout.write(result.text);
13057
+ process.exit(0);
13058
+ }
13059
+ catch (error) {
13060
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
13061
+ process.exit(2);
13062
+ }
13063
+ }
13064
+ if (cli.kind === "doctor") {
13065
+ try {
13066
+ const report = await runDoctorWithOptionalProbe({
13067
+ credentials: {
13068
+ hasPersonalAccessToken: Boolean(GITLAB_PERSONAL_ACCESS_TOKEN),
13069
+ hasJobToken: Boolean(GITLAB_JOB_TOKEN),
13070
+ hasCookie: Boolean(GITLAB_AUTH_COOKIE_PATH),
13071
+ useOAuth: USE_OAUTH,
13072
+ },
13073
+ apiUrl: GITLAB_API_URL,
13074
+ permissionMode: GITLAB_PERMISSION_MODE,
13075
+ toolsetsRaw: GITLAB_TOOLSETS_RAW,
13076
+ version: packageVersion(),
13077
+ probe: cli.probe ? probeGitLabUser : undefined,
13078
+ });
13079
+ process.stdout.write(report.text);
13080
+ process.exit(report.ok ? 0 : 1);
13081
+ }
13082
+ catch (error) {
13083
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
13084
+ process.exit(1);
13085
+ }
13086
+ }
13008
13087
  if (cli.kind === "auth" || getPositionalCliCommand(process.argv) === "auth") {
13009
13088
  try {
13010
13089
  await runAuthCommandAsync();
@@ -13053,6 +13132,32 @@ async function main() {
13053
13132
  function hasCliCredentials() {
13054
13133
  return Boolean(GITLAB_PERSONAL_ACCESS_TOKEN || GITLAB_JOB_TOKEN || GITLAB_AUTH_COOKIE_PATH || USE_OAUTH);
13055
13134
  }
13135
+ async function probeGitLabUser(apiUrl) {
13136
+ const headers = { Accept: "application/json" };
13137
+ if (GITLAB_PERSONAL_ACCESS_TOKEN) {
13138
+ headers["PRIVATE-TOKEN"] = GITLAB_PERSONAL_ACCESS_TOKEN;
13139
+ }
13140
+ else if (GITLAB_JOB_TOKEN) {
13141
+ headers["JOB-TOKEN"] = GITLAB_JOB_TOKEN;
13142
+ }
13143
+ try {
13144
+ const response = await fetch(`${apiUrl.replace(/\/$/, "")}/user`, { headers });
13145
+ if (response.ok) {
13146
+ return { ok: true, status: response.status, detail: `GET /user ${response.status}` };
13147
+ }
13148
+ return {
13149
+ ok: false,
13150
+ status: response.status,
13151
+ detail: `GET /user ${response.status} ${response.statusText}`,
13152
+ };
13153
+ }
13154
+ catch (error) {
13155
+ return {
13156
+ ok: false,
13157
+ detail: error instanceof Error ? error.message : String(error),
13158
+ };
13159
+ }
13160
+ }
13056
13161
  function isRecord(value) {
13057
13162
  return typeof value === "object" && value !== null && !Array.isArray(value);
13058
13163
  }
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"),
@@ -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,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);
@@ -0,0 +1,42 @@
1
+ const NEXT_PREFIX = "\nNext: ";
2
+ export function hasRecoveryHint(message) {
3
+ return message.includes(NEXT_PREFIX);
4
+ }
5
+ export function recoveryHintFor(message) {
6
+ if (/^Access denied:/i.test(message)) {
7
+ return undefined;
8
+ }
9
+ if (/is not available on this server/i.test(message)) {
10
+ if (/slim tool profile/i.test(message) || /cannot activate/i.test(message)) {
11
+ return undefined;
12
+ }
13
+ if (/discover_tools|GITLAB_TOOLSETS|GITLAB_TOOLS/.test(message)) {
14
+ return "call discover_tools with the category, or add it to GITLAB_TOOLSETS.";
15
+ }
16
+ }
17
+ if (/\b401\b/.test(message) || /unauthorized/i.test(message)) {
18
+ return "set GITLAB_PERSONAL_ACCESS_TOKEN (or run `zereight-mcp-gitlab auth`) and retry.";
19
+ }
20
+ if (/rate limit/i.test(message)) {
21
+ return "wait and retry; lower per_page or paginate if the client is bursting.";
22
+ }
23
+ if (/\b403\b/.test(message) ||
24
+ /forbidden/i.test(message) ||
25
+ /insufficient_scope/i.test(message)) {
26
+ return "check token scopes and GITLAB_PERMISSION_MODE; grant read_user or User:Read for whoami.";
27
+ }
28
+ if (/\b404\b/.test(message) || /404 not found/i.test(message)) {
29
+ return "check project_id (numeric ID or group%2Fproject) and the resource iid.";
30
+ }
31
+ return undefined;
32
+ }
33
+ export function appendRecoveryHint(message) {
34
+ if (hasRecoveryHint(message)) {
35
+ return message;
36
+ }
37
+ const hint = recoveryHintFor(message);
38
+ if (hint === undefined) {
39
+ return message;
40
+ }
41
+ return `${message}${NEXT_PREFIX}${hint}`;
42
+ }
@@ -0,0 +1,90 @@
1
+ /** MCP-only argument; stripped before GitLab handlers run. */
2
+ export const LIST_SUMMARY_ARGUMENT = "summary";
3
+ export const LIST_SUMMARY_ARGUMENT_DESCRIPTION = "If true, return only high-signal fields (id, iid, title, state, web_url, …). Default false keeps the full GitLab JSON.";
4
+ const SUMMARY_FIELDS = [
5
+ "id",
6
+ "iid",
7
+ "title",
8
+ "name",
9
+ "state",
10
+ "status",
11
+ "ref",
12
+ "web_url",
13
+ "path_with_namespace",
14
+ "username",
15
+ "short_id",
16
+ "key",
17
+ "tag_name",
18
+ "slug",
19
+ "filename",
20
+ "new_path",
21
+ ];
22
+ const NATIVE_SUMMARY_TOOLS = new Set(["list_webhook_events"]);
23
+ export function toolAcceptsDecoratedSummary(toolName) {
24
+ if (NATIVE_SUMMARY_TOOLS.has(toolName)) {
25
+ return false;
26
+ }
27
+ return toolName.startsWith("list_") || toolName === "mr_discussions";
28
+ }
29
+ export function readListSummaryRequested(args) {
30
+ if (!args) {
31
+ return false;
32
+ }
33
+ const raw = args[LIST_SUMMARY_ARGUMENT];
34
+ return raw === true || raw === "true";
35
+ }
36
+ export function omitListSummaryArgument(args) {
37
+ if (!(LIST_SUMMARY_ARGUMENT in args)) {
38
+ return args;
39
+ }
40
+ const { [LIST_SUMMARY_ARGUMENT]: _omit, ...rest } = args;
41
+ return rest;
42
+ }
43
+ function isRecord(value) {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ }
46
+ function summarizeItem(item) {
47
+ if (!isRecord(item)) {
48
+ return item;
49
+ }
50
+ const summarized = {};
51
+ for (const field of SUMMARY_FIELDS) {
52
+ if (field in item) {
53
+ summarized[field] = item[field];
54
+ }
55
+ }
56
+ return Object.keys(summarized).length > 0 ? summarized : item;
57
+ }
58
+ export function summarizeListPayload(data) {
59
+ if (Array.isArray(data)) {
60
+ return data.map(summarizeItem);
61
+ }
62
+ if (isRecord(data) && Array.isArray(data.items)) {
63
+ return { ...data, items: data.items.map(summarizeItem) };
64
+ }
65
+ return data;
66
+ }
67
+ function isTextContentBlock(value) {
68
+ return typeof value === "object" && value !== null;
69
+ }
70
+ export function applyListSummaryToToolResult(result, requested) {
71
+ if (!requested || result.isError) {
72
+ return result;
73
+ }
74
+ if (!Array.isArray(result.content)) {
75
+ return result;
76
+ }
77
+ const content = result.content.map(block => {
78
+ if (!isTextContentBlock(block) || block.type !== "text" || typeof block.text !== "string") {
79
+ return block;
80
+ }
81
+ try {
82
+ const parsed = JSON.parse(block.text);
83
+ return { ...block, text: JSON.stringify(summarizeListPayload(parsed)) };
84
+ }
85
+ catch {
86
+ return block;
87
+ }
88
+ });
89
+ return { ...result, content };
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.3.8-next.144",
3
+ "version": "2.3.8-next.146",
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": [