Date: 2026-09-17
Status: Implemented (generic tool layer + curated phases 1–3 + optional MCP compact)
Approach: Same binary, two doors. MCP stays the agent path. The CLI prints the full tool payload on stdout so large GitLab results do not have to live in the model context.
MCP CallTool replies are conversation tokens. Lists, diffs, and job logs are the expensive part. The CLI is the offload: run the same registry tool in a terminal and keep the chat on summaries.
MCP client ── CallTool ──> compact preview + `cli` command (opt-in)
terminal ── tool/mr ──> full unwrapped payload (always)
GITLAB_MCP_COMPACT_RESULTS is off by default. Turning it on is the only MCP reply-shape change. Without that flag, tool JSON is unchanged. Human CLI is never compacted.
Non-goals for this design:
- No new GitLab API coverage. Every command maps to an existing tool in
tools/registry.ts. - No interactive prompts in v1. Destructive commands take an explicit
--yesflag instead. - No new runtime dependencies. Output rendering is hand-rolled.
- Compact mode does not shrink ListTools schema tax. Shrink toolsets for that.
package.jsonexposes two bin aliases (mcp-gitlab,zereight-mcp-gitlab) pointing atbuild/index.js.Formula/zereight-mcp-gitlab.rbwraps the npm tarball and symlinks the bins, so the Homebrew install is already a shell-runnable binary.cli-command.ts(getPositionalCliCommand) reads the first positional arg.index.tsmain()dispatchesauth,tool <name>, and curated<group> <action>viaresolveCli, then falls through torunServer()when there is no CLI command.- All other flags (
--token,--api-url,--permission-mode, …) are server settings parsed inconfig.ts. Boolean MCP flags accept--flag,--flag=true, or--flag false; they do not swallow the next positional command. - Human CLI refuses tools that MCP would not expose: toolsets /
GITLAB_TOOLS/ legacy flags,GITLAB_DENIED_TOOLS_REGEX,GITLAB_TOOL_POLICY_HIDDEN.GITLAB_TOOL_POLICY_APPROVErequires--yes. - Tool inventory: 276 tools in
allTools(274 toolset tools plusexecute_graphqlanddiscover_tools), grouped into 24 toolsets inTOOLSET_DEFINITIONS. Theapitoolset is explicit-only (GITLAB_TOOLSETS=alldoes not enable it). Shared tools (e.g.get_branch,list_branches) belong to several toolsets, tracked byTOOLSETS_BY_TOOL_NAME.
Two layers. The generic layer guarantees full coverage with zero per-tool code. The curated layer adds short memorable forms for the tools people run every day.
zereight-mcp-gitlab auth ... # unchanged
zereight-mcp-gitlab tool <tool-name> [options] # generic, all 276 tools
zereight-mcp-gitlab <group> <action> [options] # curated, phased rollout
zereight-mcp-gitlab --help | <group> --help # generated help
zereight-mcp-gitlab tool list_issues --project-id 123 --state opened
zereight-mcp-gitlab tool get_merge_request --project-id 123 --merge-request-iid 45 --output json
zereight-mcp-gitlab tool create_issue --args-json '{"project_id":"123","title":"bug"}'- Tool name is exact (
snake_case, as in the registry). No aliases, no fuzzy matching. - Flags mirror input-schema keys in kebab-case (
project_id→--project-id). Required schema fields are required flags. --args-json '{...}'is the escape hatch for nested objects and arrays.--output json(default for this layer) prints the unwrapped payload (content[].text, JSON-parsed when possible).--output tablepicks the curated renderer when one exists for that tool, else falls back to JSON with a stderr note.--helpafter a tool name prints its description plus generated flag list fromtoJSONSchema.
Group names mirror toolset IDs in kebab-case. mr is the only short alias.
| Group | Toolset | Phase |
|---|---|---|
mr (merge-requests) |
merge_requests (45 tools) |
1 reads, 2 writes |
issue |
issues (24 tools) |
1 reads, 2 writes |
pipeline |
pipelines (58 tools) |
1 reads, 2 writes |
project |
projects (11 tools) |
1 |
branch |
branches (15 tools) |
1 |
user |
users (7 tools) |
1 (whoami) |
repo |
repositories (7 tools) |
2 |
label |
labels (5 tools) |
2 |
release |
releases (7 tools) |
2 |
tag |
tags (5 tools) |
2 |
variable |
variables (10 tools) |
2 |
milestone |
milestones (17 tools) |
3 |
wiki |
wiki (10 tools) |
3 |
group |
groups (1 tool) |
3 |
ci |
ci (4 tools) |
3 |
webhook |
webhooks (6 tools) |
3 |
workitem |
workitems (18 tools) |
3 |
search |
search (3 tools) |
3 |
vulnerability |
vulnerabilities (4 tools) |
3 |
dependency-proxy |
dependency_proxy (4 tools) |
3 |
orbit |
orbit (4 tools) |
3 |
Actions use short verbs. Each maps to exactly one tool. Curated flags may use short forms where the schema key is long (--mr-iid for merge_request_iid); the generic layer always uses exact kebab-cased schema keys.
mr list [--project-id ID | --group-id ID] [--state opened] # list_merge_requests / list_group_merge_requests
mr view --project-id ID --mr-iid N # get_merge_request
mr diff --project-id ID --mr-iid N # get_merge_request_diffs
mr files --project-id ID --mr-iid N # list_merge_request_changed_files
mr discussions --project-id ID --mr-iid N # mr_discussions
mr pipelines --project-id ID --mr-iid N # list_merge_request_pipelines
mr approvals --project-id ID --mr-iid N # get_merge_request_approval_state
issue list --project-id ID [--state opened] [--assignee-username NAME] # list_issues
issue mine # my_issues
issue view --project-id ID --issue-iid N # get_issue
issue discussions --project-id ID --issue-iid N # list_issue_discussions
pipeline list --project-id ID [--status failed] # list_pipelines
pipeline view --project-id ID --pipeline-id N # get_pipeline
pipeline jobs --project-id ID --pipeline-id N # list_pipeline_jobs
pipeline log --project-id ID --job-id N # get_pipeline_job_output
project list [--search TEXT] # list_projects
project view --project-id ID # get_project
branch list --project-id ID # list_branches
branch view --project-id ID --branch NAME # get_branch
commit list --project-id ID [--ref NAME] # list_commits
commit view --project-id ID --sha SHA # get_commit
user whoami # whoami
Phase 1 is read-only on purpose: no confirmation UX needed, safe to ship behind no flag.
Same groups, plus repo, label, release, tag, variable. Every destructive tool (delete_*, erase_*, purge_*, merge_*) requires --yes; without it the command prints what it would do and exits 2. Non-destructive writes (create_*, update_*, approve_*, retry_*) run directly but echo the resulting URL or ID.
mr create --project-id ID --source BRANCH --target BRANCH --title TEXT
mr approve --project-id ID --mr-iid N
mr merge --project-id ID --mr-iid N --yes
mr note --project-id ID --mr-iid N --body TEXT
issue create --project-id ID --title TEXT [--description TEXT]
issue close --project-id ID --issue-iid N
pipeline retry --project-id ID --pipeline-id N
Phase 3 curates the remaining groups on demand. Nothing is ever blocked: any tool without a curated form stays reachable via tool <name>.
- Auth and instance config reuse the existing surface unchanged:
--token,--api-url, plus env vars (GITLAB_PERSONAL_ACCESS_TOKEN,GITLAB_API_URL, OAuth token file). Precedence stays CLI-over-env. --permission-mode=readonly(or env) refuses write commands with a usage error (exit 2) before any network call.- Global flags:
--output table|json,--yes,--help. Resource flags are per-command, kebab-cased schema keys. --api-urlbehavior (strip/api/v4where needed) matches the server.
- Curated commands default to
--output table: one line per item for lists (ID STATE TITLE), key-value summary for single gets.--output jsonprints the unwrapped payload for scripting. - Generic
toolcommands default to--output json. - Human output goes to stdout; diagnostics, warnings, and the version notice go to stderr, so
... --output json | jqstays clean. - Errors print a one-line message (
GitLab API error: 404 Not Found) to stderr. Token and secret masking reusesutils/redact-sensitive.ts. - Exit codes:
0success,1API/auth/runtime failure,2usage error (unknown command, missing flag, refused write).
Example:
$ zereight-mcp-gitlab mr list --project-id 123 --state opened
IID STATE TITLE
45 opened fix: pipeline retry race
44 opened chore: bump deps
$ zereight-mcp-gitlab mr list --project-id 123 --output json | jq '.[0].iid'
45- New
cli/directory: router, schema-driven arg parser (readstoJSONSchemaoutput), table renderers per curated command, help generator. - Extend
getPositionalCliCommandto two levels (<group> <action>) and dispatch inmain()beforerunServer().authkeeps working as today. - Extract the
CallTooldispatch inindex.tsinto a callablerunTool(name, args)so CLI and MCP paths share one execution boundary. This is the main refactor; everything else is additive. - Update the Homebrew
testblock anddocs/getting-started/cli-arguments.mdfor the new subcommands. - Tests: unit tests for arg parsing against a sample of schemas, plus a smoke test running
tool whoami-style reads against the mock suite.
- Flags-only vs positional IDs (
mr view 123 45vs--project-id 123 --mr-iid 45)? Flags-only is proposed for uniformity with schema keys. - Table as curated default vs always-JSON? Table is proposed for shell ergonomics.
- Should the generic
toollayer be documented as advanced/hidden to steer users toward curated commands? - Destructive confirm UX:
--yesflag only, or also an interactive prompt when stdin is a TTY?