@zereight/mcp-gitlab 2.1.68 → 2.1.69
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 +5 -1
- package/README.md +5 -1
- package/README.pt-BR.md +6 -2
- package/README.zh-CN.md +5 -1
- package/build/cli/args.js +298 -0
- package/build/cli/compact-mcp-result.js +87 -0
- package/build/cli/curated.js +243 -0
- package/build/cli/exposure.js +72 -0
- package/build/cli/help.js +108 -0
- package/build/cli/output.js +65 -0
- package/build/cli/replay-command.js +53 -0
- package/build/cli/router.js +158 -0
- package/build/cli/tables.js +117 -0
- package/build/cli-boolean-flags.js +38 -0
- package/build/cli-command.js +14 -4
- package/build/config.js +30 -7
- package/build/index.js +95 -43
- package/build/oauth.js +93 -10
- package/build/scripts/generate-tool-docs.js +2 -1
- package/build/test/cli/args.test.js +175 -0
- package/build/test/cli/boolean-flags.test.js +106 -0
- package/build/test/cli/compact-mcp-result.test.js +93 -0
- package/build/test/cli/exposure.test.js +104 -0
- package/build/test/cli/load-config-snapshot.js +5 -0
- package/build/test/cli/output.test.js +74 -0
- package/build/test/cli/replay-command.test.js +37 -0
- package/build/test/cli/router.test.js +183 -0
- package/build/test/oauth-device-flow-tests.js +10 -0
- package/build/test/oauth-startup.test.js +204 -0
- package/build/test/test-dynamic-project-scope.js +2 -0
- package/build/test/test-geteffectiveprojectid.js +4 -2
- package/build/test/test-permission-mode.js +86 -0
- package/build/tools/denied-regex.js +38 -0
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -111,7 +111,7 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
|
|
|
111
111
|
|
|
112
112
|
예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
|
|
113
113
|
|
|
114
|
-
전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.
|
|
114
|
+
전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.68`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
|
|
115
115
|
|
|
116
116
|
#### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
|
|
117
117
|
|
|
@@ -139,11 +139,15 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
|
|
|
139
139
|
- `--use-milestone=true` - 마일스톤 API 활성화 (`USE_MILESTONE` 대체, 레거시 — `GITLAB_TOOLSETS=milestones` 권장)
|
|
140
140
|
- `--use-pipeline=true` - 파이프라인 API 활성화 (`USE_PIPELINE` 대체, 레거시 — `GITLAB_TOOLSETS=pipelines` 권장)
|
|
141
141
|
- `--disable-version-check=true` - 시작 시 신규 버전 알림 비활성화 (`GITLAB_DISABLE_VERSION_CHECK` 대체)
|
|
142
|
+
- `--compact-results=true` - 큰 MCP 도구 응답을 미리보기와 CLI 명령으로 바꿈 (`GITLAB_MCP_COMPACT_RESULTS` 대체, 기본 꺼짐)
|
|
143
|
+
- `--compact-result-chars` - compact 임계값 문자 수 (`GITLAB_MCP_COMPACT_RESULT_CHARS` 대체, 기본 `4000`)
|
|
142
144
|
|
|
143
145
|
CLI 인자는 환경 변수보다 우선합니다.
|
|
144
146
|
|
|
145
147
|
`zereight-mcp-gitlab auth`는 MCP 서버 플래그가 아니라 서브커맨드입니다. GitLab device flow를 실행한 뒤 종료합니다. [CLI 인자](./docs/getting-started/cli-arguments.md#auth)를 참고하세요.
|
|
146
148
|
|
|
149
|
+
같은 바이너리는 `gh` 스타일 GitLab CLI이기도 합니다 (`tool <name>` 또는 `mr list` 같은 curated 명령). 전체 페이로드는 터미널에서 이 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)를 참고하세요.
|
|
150
|
+
|
|
147
151
|
> **세밀한 도구 필터링:** `GITLAB_PERMISSION_MODE=modify`로 생성/수정은 허용하고 모든 삭제 도구와
|
|
148
152
|
> 파괴적인 중단(teardown) 도구(`cancel_pipeline`, `cancel_pipeline_job`, `stop_environment`,
|
|
149
153
|
> `stop_stale_environments`, `unprotect_branch`)를 차단하거나(`execute_graphql`을 통한 파괴적
|
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ The store path is pinned by your lock file; update it with `nix flake update git
|
|
|
116
116
|
|
|
117
117
|
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`.
|
|
118
118
|
|
|
119
|
-
No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.
|
|
119
|
+
No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.68`. 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`).
|
|
120
120
|
|
|
121
121
|
#### Using CLI Arguments (for clients with env var issues)
|
|
122
122
|
|
|
@@ -148,11 +148,15 @@ Some MCP clients (like GitHub Copilot CLI) have issues with environment variable
|
|
|
148
148
|
- `--masking-config` - Path to a masking configuration file (replaces `GITLAB_MASKING_CONFIG`)
|
|
149
149
|
- `--masking-policy-file` - Path to a protected managed-policy file (replaces `GITLAB_MASKING_POLICY_FILE`)
|
|
150
150
|
- `--masking-workspace-dir` - Directory used to resolve masking files (replaces `GITLAB_MASKING_WORKSPACE_DIR`)
|
|
151
|
+
- `--compact-results=true` - Truncate oversized MCP tool replies and attach a CLI command for the full payload (replaces `GITLAB_MCP_COMPACT_RESULTS`; default off)
|
|
152
|
+
- `--compact-result-chars` - Character threshold for compacting (replaces `GITLAB_MCP_COMPACT_RESULT_CHARS`; default `4000`)
|
|
151
153
|
|
|
152
154
|
CLI arguments take precedence over environment variables.
|
|
153
155
|
|
|
154
156
|
`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).
|
|
155
157
|
|
|
158
|
+
The same binary is also a `gh`-style GitLab CLI (`tool <name>` or curated forms such as `mr list`). 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).
|
|
159
|
+
|
|
156
160
|
> **Fine-grained tool filtering:** use `GITLAB_PERMISSION_MODE=modify` to allow create/update while
|
|
157
161
|
> blocking every delete tool and the destructive teardown tools (`cancel_pipeline`,
|
|
158
162
|
> `cancel_pipeline_job`, `stop_environment`, `stop_stale_environments`, `unprotect_branch`) —
|
package/README.pt-BR.md
CHANGED
|
@@ -111,7 +111,7 @@ O caminho no store é fixado pelo seu lock file; atualize-o com `nix flake updat
|
|
|
111
111
|
|
|
112
112
|
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`.
|
|
113
113
|
|
|
114
|
-
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.1.
|
|
114
|
+
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.1.68`. 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`).
|
|
115
115
|
|
|
116
116
|
#### Usando argumentos de CLI (para clientes com problemas com variáveis de ambiente)
|
|
117
117
|
|
|
@@ -143,11 +143,15 @@ Alguns clientes MCP, como o GitHub Copilot CLI, têm problemas com variáveis de
|
|
|
143
143
|
- `--masking-config` - caminho para um arquivo de configuração de mascaramento (substitui `GITLAB_MASKING_CONFIG`)
|
|
144
144
|
- `--masking-policy-file` - caminho para um arquivo protegido de política gerenciada (substitui `GITLAB_MASKING_POLICY_FILE`)
|
|
145
145
|
- `--masking-workspace-dir` - diretório usado para resolver arquivos de mascaramento (substitui `GITLAB_MASKING_WORKSPACE_DIR`)
|
|
146
|
+
- `--compact-results=true` - trunca respostas muito grandes de ferramentas MCP e anexa um comando CLI para a carga completa (substitui `GITLAB_MCP_COMPACT_RESULTS`; desativado por padrão)
|
|
147
|
+
- `--compact-result-chars` - limite de caracteres para compactação (substitui `GITLAB_MCP_COMPACT_RESULT_CHARS`; padrão `4000`)
|
|
146
148
|
|
|
147
149
|
Os argumentos de CLI têm precedência sobre as variáveis de ambiente.
|
|
148
150
|
|
|
149
151
|
`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).
|
|
150
152
|
|
|
153
|
+
O mesmo binário também é uma CLI do GitLab no estilo `gh` (`tool <name>` ou formas curadas como `mr list`). 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).
|
|
154
|
+
|
|
151
155
|
> **Filtragem granular de ferramentas:** use `GITLAB_PERMISSION_MODE=modify` para permitir criação/atualização enquanto
|
|
152
156
|
> bloqueia todas as ferramentas de exclusão e as ferramentas destrutivas de teardown (`cancel_pipeline`,
|
|
153
157
|
> `cancel_pipeline_job`, `stop_environment`, `stop_stale_environments`, `unprotect_branch`) —
|
|
@@ -519,7 +523,7 @@ Não é necessário definir `headers`. O Claude.ai obtém o token via OAuth.
|
|
|
519
523
|
| `STREAMABLE_HTTP` | sim | Deve ser `true` (SSE não é suportado) |
|
|
520
524
|
| `GITLAB_OAUTH_SCOPES` | não | Scopes do GitLab a solicitar, separados por vírgula. O padrão é `api`, ou `read_api` quando `GITLAB_READ_ONLY_MODE=true`. O aplicativo previamente registrado deve ter pelo menos esses scopes configurados. |
|
|
521
525
|
| `OAUTH_REGISTER_RATE_LIMIT_PER_HOUR` | não | Limite rolling de Dynamic Client Registration (`POST /register`) por IP do cliente. Padrão `20`/hora, intervalo `1`–`1000`. Aumente se múltiplas janelas de IDE estiverem atingindo o limite. Não está relacionado aos limites da API do GitLab. |
|
|
522
|
-
| `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | não | Apenas para desenvolvimento local via HTTP
|
|
526
|
+
| `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | não | Apenas para desenvolvimento local via HTTP |
|
|
523
527
|
|
|
524
528
|
**Observações importantes:**
|
|
525
529
|
|
package/README.zh-CN.md
CHANGED
|
@@ -111,7 +111,7 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
|
|
|
111
111
|
|
|
112
112
|
示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
|
|
113
113
|
|
|
114
|
-
如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.
|
|
114
|
+
如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.68`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
|
|
115
115
|
|
|
116
116
|
#### 使用 CLI 参数(适用于环境变量有问题的客户端)
|
|
117
117
|
|
|
@@ -143,11 +143,15 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
|
|
|
143
143
|
- `--masking-config` - 掩码配置文件路径(替代 `GITLAB_MASKING_CONFIG`)
|
|
144
144
|
- `--masking-policy-file` - 受保护的托管策略文件路径(替代 `GITLAB_MASKING_POLICY_FILE`)
|
|
145
145
|
- `--masking-workspace-dir` - 用于解析掩码文件的目录(替代 `GITLAB_MASKING_WORKSPACE_DIR`)
|
|
146
|
+
- `--compact-results=true` - 将过大的 MCP 工具回复截断为预览并附带 CLI 命令(替代 `GITLAB_MCP_COMPACT_RESULTS`,默认关闭)
|
|
147
|
+
- `--compact-result-chars` - compact 字符阈值(替代 `GITLAB_MCP_COMPACT_RESULT_CHARS`,默认 `4000`)
|
|
146
148
|
|
|
147
149
|
CLI 参数优先于环境变量。
|
|
148
150
|
|
|
149
151
|
`zereight-mcp-gitlab auth` 是子命令,不是 MCP 服务器参数。它运行 GitLab device flow 后退出。参见 [CLI 参数](./docs/getting-started/cli-arguments.md#auth)。
|
|
150
152
|
|
|
153
|
+
同一二进制也是 `gh` 风格的 GitLab CLI(`tool <name>`,或 `mr list` 这类 curated 命令)。完整结果在终端用 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)。
|
|
154
|
+
|
|
151
155
|
> **细粒度工具过滤:**使用 `GITLAB_PERMISSION_MODE=modify` 允许创建/更新,同时阻止所有删除工具以及
|
|
152
156
|
> 破坏性的拆除(teardown)工具(`cancel_pipeline`、`cancel_pipeline_job`、`stop_environment`、
|
|
153
157
|
> `stop_stale_environments`、`unprotect_branch`)(包括通过 `execute_graphql` 的破坏性 mutation
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { BOOLEAN_CLI_FLAG_NAMES, isBooleanFlagLiteral } from "../cli-boolean-flags.js";
|
|
2
|
+
export class CliUsageError extends Error {
|
|
3
|
+
exitCode = 2;
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "CliUsageError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const SCRIPT_PATH_PATTERN = /\.(cjs|mjs|js|cts|mts|ts)$/;
|
|
10
|
+
const FLAGS_WITHOUT_VALUE = new Set(["help", "yes", ...BOOLEAN_CLI_FLAG_NAMES]);
|
|
11
|
+
const GLOBAL_FLAG_NAMES = new Set([
|
|
12
|
+
"help",
|
|
13
|
+
"yes",
|
|
14
|
+
"output",
|
|
15
|
+
"args-json",
|
|
16
|
+
"client-id",
|
|
17
|
+
"token-path",
|
|
18
|
+
"token",
|
|
19
|
+
"job-token",
|
|
20
|
+
"cookie-path",
|
|
21
|
+
"use-oauth",
|
|
22
|
+
"is-old",
|
|
23
|
+
"read-only",
|
|
24
|
+
"masking-enabled",
|
|
25
|
+
"masking-config",
|
|
26
|
+
"masking-policy-file",
|
|
27
|
+
"masking-workspace-dir",
|
|
28
|
+
"permission-mode",
|
|
29
|
+
"use-wiki",
|
|
30
|
+
"use-milestone",
|
|
31
|
+
"use-pipeline",
|
|
32
|
+
"disable-version-check",
|
|
33
|
+
"toolsets",
|
|
34
|
+
"tools",
|
|
35
|
+
"tool-policy-approve",
|
|
36
|
+
"tool-policy-hidden",
|
|
37
|
+
"sse",
|
|
38
|
+
"streamable-http",
|
|
39
|
+
"remote-auth",
|
|
40
|
+
"mcp-oauth",
|
|
41
|
+
"allow-unauthenticated-tool-discovery",
|
|
42
|
+
"mcp-trust-proxy",
|
|
43
|
+
"mcp-server-url",
|
|
44
|
+
"oauth-app-id",
|
|
45
|
+
"oauth-scopes",
|
|
46
|
+
"oauth-callback-proxy",
|
|
47
|
+
"allowed-groups",
|
|
48
|
+
"oauth-allowed-groups",
|
|
49
|
+
"enable-dynamic-api-url",
|
|
50
|
+
"enable-dynamic-project-scope",
|
|
51
|
+
"enable-strict-project-scope",
|
|
52
|
+
"oauth-stateless-mode",
|
|
53
|
+
"oauth-stateless-client-ttl",
|
|
54
|
+
"oauth-stateless-pending-ttl",
|
|
55
|
+
"oauth-stateless-stored-ttl",
|
|
56
|
+
"session-timeout",
|
|
57
|
+
"oauth-stateless-session-ttl",
|
|
58
|
+
"host",
|
|
59
|
+
"port",
|
|
60
|
+
"http-proxy",
|
|
61
|
+
"https-proxy",
|
|
62
|
+
"no-proxy",
|
|
63
|
+
"tls-reject-unauthorized",
|
|
64
|
+
"ca-cert-path",
|
|
65
|
+
"pool-max-size",
|
|
66
|
+
"api-url",
|
|
67
|
+
"denied-tools-regex",
|
|
68
|
+
"compact-results",
|
|
69
|
+
"compact-result-chars",
|
|
70
|
+
]);
|
|
71
|
+
export function kebabToSnake(value) {
|
|
72
|
+
return value.replace(/-/g, "_");
|
|
73
|
+
}
|
|
74
|
+
export function snakeToKebab(value) {
|
|
75
|
+
return value.replace(/_/g, "-");
|
|
76
|
+
}
|
|
77
|
+
export function argvHasHelp(argv) {
|
|
78
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
79
|
+
}
|
|
80
|
+
export function parseArgv(argv) {
|
|
81
|
+
const userArgs = userArgv(argv);
|
|
82
|
+
const positionals = [];
|
|
83
|
+
const toolFlags = {};
|
|
84
|
+
let help = false;
|
|
85
|
+
let yes = false;
|
|
86
|
+
let output;
|
|
87
|
+
let argsJson;
|
|
88
|
+
for (let i = 0; i < userArgs.length; i += 1) {
|
|
89
|
+
const current = userArgs[i];
|
|
90
|
+
const flag = readFlag(current);
|
|
91
|
+
if (flag === undefined) {
|
|
92
|
+
if (current.startsWith("-")) {
|
|
93
|
+
throw new CliUsageError(`Unknown flag: ${current}`);
|
|
94
|
+
}
|
|
95
|
+
positionals.push(current);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (flag.name === "help") {
|
|
99
|
+
help = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (flag.name === "yes") {
|
|
103
|
+
if (flag.inline !== undefined) {
|
|
104
|
+
throw new CliUsageError("--yes does not accept a value");
|
|
105
|
+
}
|
|
106
|
+
yes = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (BOOLEAN_CLI_FLAG_NAMES.has(flag.name) && flag.inline === undefined) {
|
|
110
|
+
if (isBooleanFlagLiteral(userArgs[i + 1])) {
|
|
111
|
+
i += 1;
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const value = flag.inline ?? takeFlagValue(userArgs, i, flag.name);
|
|
116
|
+
if (flag.inline === undefined && !FLAGS_WITHOUT_VALUE.has(flag.name)) {
|
|
117
|
+
i += 1;
|
|
118
|
+
}
|
|
119
|
+
if (flag.name === "output") {
|
|
120
|
+
output = parseOutputMode(value);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (flag.name === "args-json") {
|
|
124
|
+
argsJson = parseArgsJson(value);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (GLOBAL_FLAG_NAMES.has(flag.name)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
toolFlags[kebabToSnake(flag.name)] = value;
|
|
131
|
+
}
|
|
132
|
+
return { positionals, toolFlags, help, yes, output, argsJson };
|
|
133
|
+
}
|
|
134
|
+
export function applyFlagAliases(flags, aliases) {
|
|
135
|
+
if (aliases === undefined) {
|
|
136
|
+
return { ...flags };
|
|
137
|
+
}
|
|
138
|
+
const mapped = {};
|
|
139
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
140
|
+
const target = aliases[key] ?? key;
|
|
141
|
+
mapped[target] = value;
|
|
142
|
+
}
|
|
143
|
+
return mapped;
|
|
144
|
+
}
|
|
145
|
+
export function parseToolArgs(input) {
|
|
146
|
+
const schema = readObjectSchema(input.schema);
|
|
147
|
+
const propertyNames = schema.properties === undefined ? new Set() : new Set(Object.keys(schema.properties));
|
|
148
|
+
const unknownFlags = Object.keys(input.flags).filter(name => !propertyNames.has(name));
|
|
149
|
+
if (unknownFlags.length > 0) {
|
|
150
|
+
throw new CliUsageError(`Unknown flag: --${snakeToKebab(unknownFlags[0] ?? "")}`);
|
|
151
|
+
}
|
|
152
|
+
const args = {};
|
|
153
|
+
assignOwn(args, input.argsJson);
|
|
154
|
+
for (const [key, value] of Object.entries(input.flags)) {
|
|
155
|
+
args[key] = coerceFlagValue(key, value, schema.properties?.[key]);
|
|
156
|
+
}
|
|
157
|
+
assignOwn(args, input.extraArgs);
|
|
158
|
+
const required = schema.required ?? [];
|
|
159
|
+
const missing = required.filter(name => args[name] === undefined);
|
|
160
|
+
if (missing.length === 1) {
|
|
161
|
+
throw new CliUsageError(`Missing required flag: --${snakeToKebab(missing[0] ?? "")}`);
|
|
162
|
+
}
|
|
163
|
+
if (missing.length > 1) {
|
|
164
|
+
throw new CliUsageError(`Missing required flags: ${missing.map(name => `--${snakeToKebab(name)}`).join(", ")}`);
|
|
165
|
+
}
|
|
166
|
+
return args;
|
|
167
|
+
}
|
|
168
|
+
function scalarFlagType(property) {
|
|
169
|
+
const rawTypes = Array.isArray(property?.type) ? property.type : [property?.type];
|
|
170
|
+
const types = rawTypes.filter(type => type !== "null");
|
|
171
|
+
if (types.length !== 1) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const [type] = types;
|
|
175
|
+
if (type === "number" || type === "integer" || type === "boolean") {
|
|
176
|
+
return type;
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
function coerceFlagValue(name, value, property) {
|
|
181
|
+
const type = scalarFlagType(property);
|
|
182
|
+
if (type === undefined) {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
const flag = `--${snakeToKebab(name)}`;
|
|
186
|
+
if (type === "boolean") {
|
|
187
|
+
const normalized = value.toLowerCase();
|
|
188
|
+
if (normalized === "true" || normalized === "1") {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
if (normalized === "false" || normalized === "0") {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
throw new CliUsageError(`${flag} must be true or false`);
|
|
195
|
+
}
|
|
196
|
+
const parsed = value.trim() === "" ? Number.NaN : Number(value);
|
|
197
|
+
if (!Number.isFinite(parsed)) {
|
|
198
|
+
throw new CliUsageError(`${flag} must be a number`);
|
|
199
|
+
}
|
|
200
|
+
if (type === "integer" && !Number.isInteger(parsed)) {
|
|
201
|
+
throw new CliUsageError(`${flag} must be an integer`);
|
|
202
|
+
}
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
export function readObjectSchema(schema) {
|
|
206
|
+
if (!isObject(schema)) {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
const propertiesValue = schema.properties;
|
|
210
|
+
const requiredValue = schema.required;
|
|
211
|
+
const properties = isObject(propertiesValue)
|
|
212
|
+
? Object.fromEntries(Object.entries(propertiesValue).flatMap(([key, value]) => isObject(value) ? [[key, value]] : []))
|
|
213
|
+
: undefined;
|
|
214
|
+
const required = Array.isArray(requiredValue)
|
|
215
|
+
? requiredValue.filter((item) => typeof item === "string")
|
|
216
|
+
: undefined;
|
|
217
|
+
return {
|
|
218
|
+
type: schema.type,
|
|
219
|
+
properties,
|
|
220
|
+
required,
|
|
221
|
+
description: schema.description,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
export function isCliOutputMode(value) {
|
|
225
|
+
return value === "json" || value === "table";
|
|
226
|
+
}
|
|
227
|
+
function userArgv(argv) {
|
|
228
|
+
const args = argv.slice(2);
|
|
229
|
+
const start = args.findIndex(arg => !arg.startsWith("-") && SCRIPT_PATH_PATTERN.test(arg));
|
|
230
|
+
if (start >= 0) {
|
|
231
|
+
return args.slice(start + 1);
|
|
232
|
+
}
|
|
233
|
+
return args;
|
|
234
|
+
}
|
|
235
|
+
function readFlag(arg) {
|
|
236
|
+
if (arg === "-h") {
|
|
237
|
+
return { name: "help", inline: undefined };
|
|
238
|
+
}
|
|
239
|
+
if (!arg.startsWith("--") || arg === "--") {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
const body = arg.slice(2);
|
|
243
|
+
const separator = body.indexOf("=");
|
|
244
|
+
if (separator === -1) {
|
|
245
|
+
return { name: body, inline: undefined };
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
name: body.slice(0, separator),
|
|
249
|
+
inline: body.slice(separator + 1),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function takeFlagValue(args, index, flagName) {
|
|
253
|
+
if (FLAGS_WITHOUT_VALUE.has(flagName)) {
|
|
254
|
+
return "true";
|
|
255
|
+
}
|
|
256
|
+
const next = args[index + 1];
|
|
257
|
+
if (next === undefined || next.startsWith("-")) {
|
|
258
|
+
throw new CliUsageError(`Missing value for --${flagName}`);
|
|
259
|
+
}
|
|
260
|
+
return next;
|
|
261
|
+
}
|
|
262
|
+
function parseOutputMode(value) {
|
|
263
|
+
if (isCliOutputMode(value)) {
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
throw new CliUsageError("--output must be table or json");
|
|
267
|
+
}
|
|
268
|
+
function parseArgsJson(raw) {
|
|
269
|
+
try {
|
|
270
|
+
const parsed = JSON.parse(raw);
|
|
271
|
+
if (!isObject(parsed)) {
|
|
272
|
+
throw new CliUsageError("--args-json must be a JSON object");
|
|
273
|
+
}
|
|
274
|
+
const copy = {};
|
|
275
|
+
assignOwn(copy, parsed);
|
|
276
|
+
return copy;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
if (error instanceof CliUsageError) {
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
throw new CliUsageError("Invalid --args-json");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function assignOwn(target, source) {
|
|
286
|
+
if (source === undefined) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
for (const key of Object.keys(source)) {
|
|
290
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
target[key] = Reflect.get(source, key);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function isObject(value) {
|
|
297
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
298
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { buildToolReplayCommand } from "./replay-command.js";
|
|
2
|
+
const PREVIEW_ARRAY_ITEMS = 5;
|
|
3
|
+
const PREVIEW_TEXT_CHARS = 1500;
|
|
4
|
+
const PREVIEW_JSON_CHARS = 2000;
|
|
5
|
+
export function readOwnRecord(value) {
|
|
6
|
+
if (!isRecord(value)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const copy = {};
|
|
10
|
+
for (const key of Object.keys(value)) {
|
|
11
|
+
copy[key] = Reflect.get(value, key);
|
|
12
|
+
}
|
|
13
|
+
return copy;
|
|
14
|
+
}
|
|
15
|
+
export function compactMcpToolResult(input) {
|
|
16
|
+
if (!input.enabled || input.toolName === "discover_tools") {
|
|
17
|
+
return toolResultRecord(input.result);
|
|
18
|
+
}
|
|
19
|
+
if (!isToolResult(input.result) || input.result.isError === true) {
|
|
20
|
+
return toolResultRecord(input.result);
|
|
21
|
+
}
|
|
22
|
+
const text = unwrapToolText(input.result);
|
|
23
|
+
if (text.length <= input.maxChars) {
|
|
24
|
+
return toolResultRecord(input.result);
|
|
25
|
+
}
|
|
26
|
+
const compact = {
|
|
27
|
+
compact: true,
|
|
28
|
+
tool: input.toolName,
|
|
29
|
+
original_chars: text.length,
|
|
30
|
+
...buildPreview(text),
|
|
31
|
+
cli: buildToolReplayCommand(input.toolName, input.args),
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: JSON.stringify(compact) }],
|
|
35
|
+
isError: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function toolResultRecord(value) {
|
|
39
|
+
if (isRecord(value)) {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
return { content: [{ type: "text", text: stringifyUnknown(value) }] };
|
|
43
|
+
}
|
|
44
|
+
function buildPreview(text) {
|
|
45
|
+
const parsed = parseJsonValue(text);
|
|
46
|
+
if (parsed === undefined) {
|
|
47
|
+
return { preview: text.slice(0, PREVIEW_TEXT_CHARS) };
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(parsed)) {
|
|
50
|
+
return { preview: parsed.slice(0, PREVIEW_ARRAY_ITEMS), items_total: parsed.length };
|
|
51
|
+
}
|
|
52
|
+
if (typeof parsed === "string") {
|
|
53
|
+
return { preview: parsed.slice(0, PREVIEW_TEXT_CHARS) };
|
|
54
|
+
}
|
|
55
|
+
const encoded = JSON.stringify(parsed);
|
|
56
|
+
if (encoded.length <= PREVIEW_JSON_CHARS) {
|
|
57
|
+
return { preview: parsed };
|
|
58
|
+
}
|
|
59
|
+
return { preview: encoded.slice(0, PREVIEW_JSON_CHARS) };
|
|
60
|
+
}
|
|
61
|
+
function unwrapToolText(result) {
|
|
62
|
+
const textBlock = result.content.find(item => isRecord(item) && typeof item.text === "string");
|
|
63
|
+
if (!isRecord(textBlock) || typeof textBlock.text !== "string") {
|
|
64
|
+
return stringifyUnknown(result);
|
|
65
|
+
}
|
|
66
|
+
return textBlock.text;
|
|
67
|
+
}
|
|
68
|
+
function isToolResult(value) {
|
|
69
|
+
return isRecord(value) && Array.isArray(value.content);
|
|
70
|
+
}
|
|
71
|
+
function parseJsonValue(text) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(text);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function stringifyUnknown(value) {
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
return JSON.stringify(value);
|
|
84
|
+
}
|
|
85
|
+
function isRecord(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|