@zereight/mcp-gitlab 2.1.68 → 2.1.70

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.
Files changed (40) hide show
  1. package/README.ko.md +11 -3
  2. package/README.md +237 -232
  3. package/README.pt-BR.md +8 -4
  4. package/README.zh-CN.md +7 -3
  5. package/build/cli/args.js +298 -0
  6. package/build/cli/compact-mcp-result.js +87 -0
  7. package/build/cli/curated.js +243 -0
  8. package/build/cli/exposure.js +72 -0
  9. package/build/cli/help.js +108 -0
  10. package/build/cli/output.js +65 -0
  11. package/build/cli/replay-command.js +53 -0
  12. package/build/cli/router.js +158 -0
  13. package/build/cli/tables.js +117 -0
  14. package/build/cli-boolean-flags.js +38 -0
  15. package/build/cli-command.js +14 -4
  16. package/build/config.js +30 -7
  17. package/build/index.js +176 -90
  18. package/build/oauth.js +93 -10
  19. package/build/scripts/generate-tool-docs.js +2 -1
  20. package/build/test/cli/args.test.js +175 -0
  21. package/build/test/cli/boolean-flags.test.js +106 -0
  22. package/build/test/cli/compact-mcp-result.test.js +93 -0
  23. package/build/test/cli/credentials.test.js +153 -0
  24. package/build/test/cli/exposure.test.js +104 -0
  25. package/build/test/cli/load-config-snapshot.js +5 -0
  26. package/build/test/cli/output.test.js +74 -0
  27. package/build/test/cli/replay-command.test.js +37 -0
  28. package/build/test/cli/router.test.js +183 -0
  29. package/build/test/gitlab-api-tool-errors.test.js +112 -0
  30. package/build/test/oauth-device-flow-tests.js +10 -0
  31. package/build/test/oauth-download-url.test.js +157 -0
  32. package/build/test/oauth-startup.test.js +204 -0
  33. package/build/test/test-dynamic-project-scope.js +2 -0
  34. package/build/test/test-geteffectiveprojectid.js +20 -26
  35. package/build/test/test-permission-mode.js +86 -0
  36. package/build/test/test-pipeline-schedules.js +3 -1
  37. package/build/test/utils/url.test.js +14 -1
  38. package/build/tools/denied-regex.js +38 -0
  39. package/build/utils/url.js +15 -0
  40. package/package.json +1 -1
@@ -0,0 +1,243 @@
1
+ import { CliUsageError } from "./args.js";
2
+ const MR_IID = { mr_iid: "merge_request_iid", iid: "merge_request_iid" };
3
+ const ISSUE_IID = { issue_iid: "issue_iid", iid: "issue_iid" };
4
+ const BRANCH = { branch: "branch_name" };
5
+ const SOURCE_TARGET = { source: "source_branch", target: "target_branch" };
6
+ const LIST_MR = { kind: "list", columns: ["iid", "state", "title"] };
7
+ const KV_MR = { kind: "kv", columns: ["iid", "state", "title", "source_branch", "target_branch", "web_url"] };
8
+ const LIST_ISSUE = { kind: "list", columns: ["iid", "state", "title"] };
9
+ const KV_ISSUE = { kind: "kv", columns: ["iid", "state", "title", "web_url"] };
10
+ const LIST_PIPELINE = { kind: "list", columns: ["id", "status", "ref"] };
11
+ const KV_PIPELINE = { kind: "kv", columns: ["id", "status", "ref", "web_url"] };
12
+ const LIST_JOB = { kind: "list", columns: ["id", "name", "status"] };
13
+ const LIST_PROJECT = { kind: "list", columns: ["id", "path_with_namespace"] };
14
+ const KV_PROJECT = { kind: "kv", columns: ["id", "name", "path_with_namespace", "web_url"] };
15
+ const LIST_BRANCH = { kind: "list", columns: ["name", "merged", "protected"] };
16
+ const KV_BRANCH = { kind: "kv", columns: ["name", "merged", "protected", "default"] };
17
+ const LIST_COMMIT = { kind: "list", columns: ["short_id", "title", "author_name"] };
18
+ const KV_COMMIT = { kind: "kv", columns: ["id", "title", "author_name", "web_url"] };
19
+ const KV_USER = { kind: "kv", columns: ["id", "username", "name", "web_url"] };
20
+ const LIST_LABEL = { kind: "list", columns: ["id", "name", "color"] };
21
+ const LIST_RELEASE = { kind: "list", columns: ["tag_name", "name"] };
22
+ const LIST_TAG = { kind: "list", columns: ["name", "message"] };
23
+ const LIST_VAR = { kind: "list", columns: ["key", "protected", "masked"] };
24
+ const LIST_MILESTONE = { kind: "list", columns: ["iid", "title", "state"] };
25
+ const LIST_WIKI = { kind: "list", columns: ["slug", "title"] };
26
+ const LIST_WEBHOOK = { kind: "list", columns: ["id", "url"] };
27
+ const LIST_WORKITEM = { kind: "list", columns: ["iid", "title", "work_item_type"] };
28
+ const TEXT = { kind: "text" };
29
+ const GROUP_ALIASES = {
30
+ mr: "mr",
31
+ "merge-requests": "mr",
32
+ issue: "issue",
33
+ issues: "issue",
34
+ pipeline: "pipeline",
35
+ pipelines: "pipeline",
36
+ project: "project",
37
+ projects: "project",
38
+ branch: "branch",
39
+ branches: "branch",
40
+ commit: "commit",
41
+ commits: "commit",
42
+ user: "user",
43
+ users: "user",
44
+ repo: "repo",
45
+ repository: "repo",
46
+ repositories: "repo",
47
+ label: "label",
48
+ labels: "label",
49
+ release: "release",
50
+ releases: "release",
51
+ tag: "tag",
52
+ tags: "tag",
53
+ variable: "variable",
54
+ variables: "variable",
55
+ milestone: "milestone",
56
+ milestones: "milestone",
57
+ wiki: "wiki",
58
+ group: "group",
59
+ groups: "group",
60
+ ci: "ci",
61
+ webhook: "webhook",
62
+ webhooks: "webhook",
63
+ workitem: "workitem",
64
+ workitems: "workitem",
65
+ search: "search",
66
+ vulnerability: "vulnerability",
67
+ vulnerabilities: "vulnerability",
68
+ "dependency-proxy": "dependency-proxy",
69
+ orbit: "orbit",
70
+ };
71
+ export const CURATED_COMMANDS = [
72
+ // Phase 1 reads
73
+ { group: "mr", action: "list", tool: "list_merge_requests", scope: { projectTool: "list_merge_requests", groupTool: "list_group_merge_requests" }, table: LIST_MR },
74
+ { group: "mr", action: "view", tool: "get_merge_request", flagAliases: MR_IID, table: KV_MR },
75
+ { group: "mr", action: "diff", tool: "get_merge_request_diffs", flagAliases: MR_IID, table: TEXT },
76
+ { group: "mr", action: "files", tool: "list_merge_request_changed_files", flagAliases: MR_IID, table: { kind: "list", columns: ["new_path", "old_path"] } },
77
+ { group: "mr", action: "discussions", tool: "mr_discussions", flagAliases: MR_IID, table: TEXT },
78
+ { group: "mr", action: "pipelines", tool: "list_merge_request_pipelines", flagAliases: MR_IID, table: LIST_PIPELINE },
79
+ { group: "mr", action: "approvals", tool: "get_merge_request_approval_state", flagAliases: MR_IID, table: TEXT },
80
+ { group: "issue", action: "list", tool: "list_issues", table: LIST_ISSUE },
81
+ { group: "issue", action: "mine", tool: "my_issues", table: LIST_ISSUE },
82
+ { group: "issue", action: "view", tool: "get_issue", flagAliases: ISSUE_IID, table: KV_ISSUE },
83
+ { group: "issue", action: "discussions", tool: "list_issue_discussions", flagAliases: ISSUE_IID, table: TEXT },
84
+ { group: "pipeline", action: "list", tool: "list_pipelines", table: LIST_PIPELINE },
85
+ { group: "pipeline", action: "view", tool: "get_pipeline", table: KV_PIPELINE },
86
+ { group: "pipeline", action: "jobs", tool: "list_pipeline_jobs", table: LIST_JOB },
87
+ { group: "pipeline", action: "log", tool: "get_pipeline_job_output", table: TEXT },
88
+ { group: "project", action: "list", tool: "list_projects", table: LIST_PROJECT },
89
+ { group: "project", action: "view", tool: "get_project", table: KV_PROJECT },
90
+ { group: "branch", action: "list", tool: "list_branches", table: LIST_BRANCH },
91
+ { group: "branch", action: "view", tool: "get_branch", flagAliases: BRANCH, table: KV_BRANCH },
92
+ { group: "commit", action: "list", tool: "list_commits", table: LIST_COMMIT },
93
+ { group: "commit", action: "view", tool: "get_commit", table: KV_COMMIT },
94
+ { group: "user", action: "whoami", tool: "whoami", table: KV_USER },
95
+ // Phase 2 writes + extra daily commands
96
+ { group: "mr", action: "create", tool: "create_merge_request", flagAliases: SOURCE_TARGET, table: KV_MR },
97
+ { group: "mr", action: "approve", tool: "approve_merge_request", flagAliases: MR_IID, table: KV_MR },
98
+ { group: "mr", action: "unapprove", tool: "unapprove_merge_request", flagAliases: MR_IID, table: KV_MR },
99
+ { group: "mr", action: "merge", tool: "merge_merge_request", flagAliases: MR_IID, table: KV_MR },
100
+ { group: "mr", action: "note", tool: "create_merge_request_note", flagAliases: MR_IID, table: { kind: "kv", columns: ["id", "body", "created_at"] } },
101
+ { group: "mr", action: "update", tool: "update_merge_request", flagAliases: MR_IID, table: KV_MR },
102
+ { group: "issue", action: "create", tool: "create_issue", table: KV_ISSUE },
103
+ { group: "issue", action: "close", tool: "update_issue", extraArgs: { state_event: "close" }, flagAliases: ISSUE_IID, table: KV_ISSUE },
104
+ { group: "issue", action: "reopen", tool: "update_issue", extraArgs: { state_event: "reopen" }, flagAliases: ISSUE_IID, table: KV_ISSUE },
105
+ { group: "issue", action: "update", tool: "update_issue", flagAliases: ISSUE_IID, table: KV_ISSUE },
106
+ { group: "issue", action: "note", tool: "create_issue_note", flagAliases: ISSUE_IID, table: { kind: "kv", columns: ["id", "body"] } },
107
+ { group: "issue", action: "delete", tool: "delete_issue", flagAliases: ISSUE_IID, table: TEXT },
108
+ { group: "pipeline", action: "retry", tool: "retry_pipeline", table: KV_PIPELINE },
109
+ { group: "pipeline", action: "cancel", tool: "cancel_pipeline", table: KV_PIPELINE },
110
+ { group: "pipeline", action: "create", tool: "create_pipeline", table: KV_PIPELINE },
111
+ { group: "pipeline", action: "delete", tool: "delete_pipeline", table: TEXT },
112
+ { group: "pipeline", action: "job-retry", tool: "retry_pipeline_job", table: LIST_JOB },
113
+ { group: "pipeline", action: "job-cancel", tool: "cancel_pipeline_job", table: LIST_JOB },
114
+ { group: "pipeline", action: "job-play", tool: "play_pipeline_job", table: LIST_JOB },
115
+ { group: "project", action: "members", tool: "list_project_members", table: { kind: "list", columns: ["id", "username", "access_level"] } },
116
+ { group: "project", action: "update", tool: "update_project", table: KV_PROJECT },
117
+ { group: "branch", action: "create", tool: "create_branch", table: KV_BRANCH },
118
+ { group: "branch", action: "delete", tool: "delete_branch", flagAliases: BRANCH, table: TEXT },
119
+ { group: "commit", action: "diff", tool: "get_commit_diff", table: TEXT },
120
+ { group: "user", action: "list", tool: "get_users", table: { kind: "list", columns: ["id", "username", "name"] } },
121
+ { group: "user", action: "view", tool: "get_user", table: KV_USER },
122
+ { group: "user", action: "events", tool: "list_events", table: { kind: "list", columns: ["action_name", "target_type", "created_at"] } },
123
+ { group: "repo", action: "search", tool: "search_repositories", table: LIST_PROJECT },
124
+ { group: "repo", action: "tree", tool: "get_repository_tree", table: { kind: "list", columns: ["path", "type"] } },
125
+ { group: "repo", action: "file", tool: "get_file_contents", table: TEXT },
126
+ { group: "repo", action: "create", tool: "create_repository", table: KV_PROJECT },
127
+ { group: "repo", action: "fork", tool: "fork_repository", table: KV_PROJECT },
128
+ { group: "repo", action: "push", tool: "push_files", table: TEXT },
129
+ { group: "label", action: "list", tool: "list_labels", table: LIST_LABEL },
130
+ { group: "label", action: "view", tool: "get_label", table: { kind: "kv", columns: ["id", "name", "color", "description"] } },
131
+ { group: "label", action: "create", tool: "create_label", table: LIST_LABEL },
132
+ { group: "label", action: "update", tool: "update_label", table: LIST_LABEL },
133
+ { group: "label", action: "delete", tool: "delete_label", table: TEXT },
134
+ { group: "release", action: "list", tool: "list_releases", table: LIST_RELEASE },
135
+ { group: "release", action: "view", tool: "get_release", table: { kind: "kv", columns: ["tag_name", "name", "description"] } },
136
+ { group: "release", action: "create", tool: "create_release", table: LIST_RELEASE },
137
+ { group: "release", action: "update", tool: "update_release", table: LIST_RELEASE },
138
+ { group: "release", action: "delete", tool: "delete_release", table: TEXT },
139
+ { group: "release", action: "download", tool: "download_release_asset", table: TEXT },
140
+ { group: "tag", action: "list", tool: "list_tags", table: LIST_TAG },
141
+ { group: "tag", action: "view", tool: "get_tag", table: { kind: "kv", columns: ["name", "message", "target"] } },
142
+ { group: "tag", action: "create", tool: "create_tag", table: LIST_TAG },
143
+ { group: "tag", action: "delete", tool: "delete_tag", table: TEXT },
144
+ { group: "tag", action: "signature", tool: "get_tag_signature", table: TEXT },
145
+ { group: "variable", action: "list", tool: "list_project_variables", scope: { projectTool: "list_project_variables", groupTool: "list_group_variables" }, table: LIST_VAR },
146
+ { group: "variable", action: "view", tool: "get_project_variable", scope: { projectTool: "get_project_variable", groupTool: "get_group_variable" }, table: { kind: "kv", columns: ["key", "variable_type", "protected", "masked"] } },
147
+ { group: "variable", action: "create", tool: "create_project_variable", scope: { projectTool: "create_project_variable", groupTool: "create_group_variable" }, table: LIST_VAR },
148
+ { group: "variable", action: "update", tool: "update_project_variable", scope: { projectTool: "update_project_variable", groupTool: "update_group_variable" }, table: LIST_VAR },
149
+ { group: "variable", action: "delete", tool: "delete_project_variable", scope: { projectTool: "delete_project_variable", groupTool: "delete_group_variable" }, table: TEXT },
150
+ // Phase 3 remaining groups
151
+ { group: "milestone", action: "list", tool: "list_milestones", scope: { projectTool: "list_milestones", groupTool: "list_group_milestones" }, table: LIST_MILESTONE },
152
+ { group: "milestone", action: "view", tool: "get_milestone", scope: { projectTool: "get_milestone", groupTool: "get_group_milestone" }, table: { kind: "kv", columns: ["iid", "title", "state", "web_url"] } },
153
+ { group: "milestone", action: "create", tool: "create_milestone", scope: { projectTool: "create_milestone", groupTool: "create_group_milestone" }, table: LIST_MILESTONE },
154
+ { group: "milestone", action: "edit", tool: "edit_milestone", scope: { projectTool: "edit_milestone", groupTool: "edit_group_milestone" }, table: LIST_MILESTONE },
155
+ { group: "milestone", action: "delete", tool: "delete_milestone", scope: { projectTool: "delete_milestone", groupTool: "delete_group_milestone" }, table: TEXT },
156
+ { group: "milestone", action: "issues", tool: "get_milestone_issue", scope: { projectTool: "get_milestone_issue", groupTool: "get_group_milestone_issue" }, table: LIST_ISSUE },
157
+ { group: "milestone", action: "mrs", tool: "get_milestone_merge_requests", scope: { projectTool: "get_milestone_merge_requests", groupTool: "get_group_milestone_merge_requests" }, table: LIST_MR },
158
+ { group: "wiki", action: "list", tool: "list_wiki_pages", scope: { projectTool: "list_wiki_pages", groupTool: "list_group_wiki_pages" }, table: LIST_WIKI },
159
+ { group: "wiki", action: "view", tool: "get_wiki_page", scope: { projectTool: "get_wiki_page", groupTool: "get_group_wiki_page" }, table: { kind: "kv", columns: ["slug", "title", "content"] } },
160
+ { group: "wiki", action: "create", tool: "create_wiki_page", scope: { projectTool: "create_wiki_page", groupTool: "create_group_wiki_page" }, table: LIST_WIKI },
161
+ { group: "wiki", action: "update", tool: "update_wiki_page", scope: { projectTool: "update_wiki_page", groupTool: "update_group_wiki_page" }, table: LIST_WIKI },
162
+ { group: "wiki", action: "delete", tool: "delete_wiki_page", scope: { projectTool: "delete_wiki_page", groupTool: "delete_group_wiki_page" }, table: TEXT },
163
+ { group: "group", action: "create", tool: "create_group", table: { kind: "kv", columns: ["id", "name", "full_path", "web_url"] } },
164
+ { group: "ci", action: "lint", tool: "validate_ci_lint", table: TEXT },
165
+ { group: "ci", action: "project-lint", tool: "validate_project_ci_lint", table: TEXT },
166
+ { group: "ci", action: "catalog", tool: "list_ci_catalog_resources", table: { kind: "list", columns: ["id", "name"] } },
167
+ { group: "ci", action: "catalog-view", tool: "get_ci_catalog_resource", table: TEXT },
168
+ { group: "webhook", action: "list", tool: "list_webhooks", table: LIST_WEBHOOK },
169
+ { group: "webhook", action: "create", tool: "create_webhook", table: LIST_WEBHOOK },
170
+ { group: "webhook", action: "update", tool: "update_webhook", table: LIST_WEBHOOK },
171
+ { group: "webhook", action: "delete", tool: "delete_webhook", table: TEXT },
172
+ { group: "webhook", action: "events", tool: "list_webhook_events", table: TEXT },
173
+ { group: "webhook", action: "event", tool: "get_webhook_event", table: TEXT },
174
+ { group: "workitem", action: "list", tool: "list_work_items", table: LIST_WORKITEM },
175
+ { group: "workitem", action: "view", tool: "get_work_item", table: { kind: "kv", columns: ["iid", "title", "web_url"] } },
176
+ { group: "workitem", action: "create", tool: "create_work_item", table: LIST_WORKITEM },
177
+ { group: "workitem", action: "update", tool: "update_work_item", table: LIST_WORKITEM },
178
+ { group: "workitem", action: "convert", tool: "convert_work_item_type", table: LIST_WORKITEM },
179
+ { group: "workitem", action: "move", tool: "move_work_item", table: LIST_WORKITEM },
180
+ { group: "workitem", action: "notes", tool: "list_work_item_notes", table: TEXT },
181
+ { group: "workitem", action: "note", tool: "create_work_item_note", table: TEXT },
182
+ { group: "search", action: "code", tool: "search_code", table: TEXT },
183
+ { group: "search", action: "project", tool: "search_project_code", table: TEXT },
184
+ { group: "search", action: "group", tool: "search_group_code", table: TEXT },
185
+ { group: "vulnerability", action: "list", tool: "list_project_vulnerabilities", table: TEXT },
186
+ { group: "vulnerability", action: "view", tool: "get_vulnerability", table: TEXT },
187
+ { group: "vulnerability", action: "dismiss", tool: "dismiss_vulnerability", table: TEXT },
188
+ { group: "vulnerability", action: "confirm", tool: "confirm_vulnerability", table: TEXT },
189
+ { group: "dependency-proxy", action: "settings", tool: "get_dependency_proxy_settings", table: TEXT },
190
+ { group: "dependency-proxy", action: "update-settings", tool: "update_dependency_proxy_settings", table: TEXT },
191
+ { group: "dependency-proxy", action: "blobs", tool: "list_dependency_proxy_blobs", table: TEXT },
192
+ { group: "dependency-proxy", action: "purge", tool: "purge_dependency_proxy_cache", table: TEXT },
193
+ { group: "orbit", action: "query", tool: "orbit_query", table: TEXT },
194
+ { group: "orbit", action: "schema", tool: "orbit_get_schema", table: TEXT },
195
+ { group: "orbit", action: "status", tool: "orbit_get_status", table: TEXT },
196
+ { group: "orbit", action: "tools", tool: "orbit_list_tools", table: TEXT },
197
+ ];
198
+ export function canonicalGroup(name) {
199
+ return GROUP_ALIASES[name];
200
+ }
201
+ export function isKnownCliCommand(name) {
202
+ return name === "auth" || name === "tool" || canonicalGroup(name) !== undefined;
203
+ }
204
+ export function findCuratedCommand(group, action) {
205
+ const canonical = canonicalGroup(group);
206
+ if (canonical === undefined) {
207
+ return undefined;
208
+ }
209
+ return CURATED_COMMANDS.find(command => command.group === canonical && command.action === action);
210
+ }
211
+ export function listGroupActions(group) {
212
+ const canonical = canonicalGroup(group);
213
+ if (canonical === undefined) {
214
+ return [];
215
+ }
216
+ return CURATED_COMMANDS.filter(command => command.group === canonical);
217
+ }
218
+ export function listGroups() {
219
+ const seen = new Set();
220
+ const groups = [];
221
+ for (const command of CURATED_COMMANDS) {
222
+ if (!seen.has(command.group)) {
223
+ seen.add(command.group);
224
+ groups.push(command.group);
225
+ }
226
+ }
227
+ return groups;
228
+ }
229
+ export function resolveScopedTool(command, flags) {
230
+ const scope = command.scope;
231
+ if (scope === undefined) {
232
+ return command.tool;
233
+ }
234
+ const hasGroup = flags.group_id !== undefined;
235
+ const hasProject = flags.project_id !== undefined;
236
+ if (hasGroup && hasProject) {
237
+ throw new CliUsageError("use --project-id or --group-id, not both");
238
+ }
239
+ if (hasGroup) {
240
+ return scope.groupTool;
241
+ }
242
+ return scope.projectTool;
243
+ }
@@ -0,0 +1,72 @@
1
+ import { GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_TOOLS_RAW, GITLAB_TOOLSETS_RAW, getConfig, } from "../config.js";
2
+ import { compileDeniedToolsRegex } from "../tools/denied-regex.js";
3
+ import { buildFeatureFlagOverrides, isToolInEnabledToolset, parseEnabledToolsets, parseIndividualTools, } from "../tools/registry.js";
4
+ export function parseToolNameSet(raw) {
5
+ if (!raw || raw.trim() === "") {
6
+ return new Set();
7
+ }
8
+ return new Set(raw
9
+ .split(",")
10
+ .map(name => name.trim())
11
+ .filter(Boolean));
12
+ }
13
+ export function isToolExposed(input) {
14
+ if (input.hiddenTools.has(input.toolName)) {
15
+ return false;
16
+ }
17
+ if (input.deniedRegex?.test(input.toolName) === true) {
18
+ return false;
19
+ }
20
+ if (isToolInEnabledToolset(input.toolName, input.enabledToolsets)) {
21
+ return true;
22
+ }
23
+ if (input.individuallyEnabledTools.has(input.toolName)) {
24
+ return true;
25
+ }
26
+ return input.featureFlagOverrides.has(input.toolName);
27
+ }
28
+ export function exposureRefusalMessage(input) {
29
+ if (input.hiddenTools.has(input.toolName)) {
30
+ return `${input.toolName} is hidden by GITLAB_TOOL_POLICY_HIDDEN`;
31
+ }
32
+ if (input.deniedRegex?.test(input.toolName) === true) {
33
+ return `${input.toolName} is blocked by GITLAB_DENIED_TOOLS_REGEX`;
34
+ }
35
+ if (isToolExposed(input)) {
36
+ return undefined;
37
+ }
38
+ return `${input.toolName} is not enabled by GITLAB_TOOLSETS, GITLAB_TOOLS, or legacy wiki/milestone/pipeline flags`;
39
+ }
40
+ export function liveCliExposure() {
41
+ const enabledToolsets = parseEnabledToolsets(GITLAB_TOOLSETS_RAW);
42
+ const individuallyEnabledTools = parseIndividualTools(GITLAB_TOOLS_RAW);
43
+ const featureFlagOverrides = buildFeatureFlagOverrides();
44
+ const deniedRegex = compileDeniedToolsRegex(getConfig("denied-tools-regex", "GITLAB_DENIED_TOOLS_REGEX")).regex;
45
+ const hiddenTools = parseToolNameSet(GITLAB_TOOL_POLICY_HIDDEN_RAW);
46
+ const approveTools = parseToolNameSet(GITLAB_TOOL_POLICY_APPROVE_RAW);
47
+ return {
48
+ isExposed(toolName) {
49
+ return isToolExposed({
50
+ toolName,
51
+ enabledToolsets,
52
+ individuallyEnabledTools,
53
+ featureFlagOverrides,
54
+ deniedRegex,
55
+ hiddenTools,
56
+ });
57
+ },
58
+ needsConfirmation(toolName) {
59
+ return approveTools.has(toolName);
60
+ },
61
+ exposureRefusal(toolName) {
62
+ return exposureRefusalMessage({
63
+ toolName,
64
+ enabledToolsets,
65
+ individuallyEnabledTools,
66
+ featureFlagOverrides,
67
+ deniedRegex,
68
+ hiddenTools,
69
+ });
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,108 @@
1
+ import { allTools } from "../tools/registry.js";
2
+ import { readObjectSchema, snakeToKebab } from "./args.js";
3
+ import { CURATED_COMMANDS, listGroupActions, listGroups } from "./curated.js";
4
+ const BINARY = "zereight-mcp-gitlab";
5
+ export function globalHelpText() {
6
+ const groups = listGroups().map(group => ` ${group}`).join("\n");
7
+ return `${BINARY} — GitLab MCP server and human CLI
8
+
9
+ Usage:
10
+ ${BINARY} auth [options]
11
+ ${BINARY} tool <tool-name> [options]
12
+ ${BINARY} <group> <action> [options]
13
+ ${BINARY} --help
14
+
15
+ Global flags:
16
+ --output table|json Output format (table default for curated, json for tool)
17
+ --yes Confirm destructive or approval-policy commands
18
+ --args-json '{...}' Nested/array arguments
19
+ --token TOKEN GitLab personal access token
20
+ --api-url URL GitLab API URL
21
+ --permission-mode readonly | modify | full
22
+
23
+ Groups:
24
+ ${groups}
25
+
26
+ Advanced: ${BINARY} tool <name> covers registry tools allowed by the current toolsets and filters. Curated commands are the daily path.
27
+ Human CLI honors GITLAB_TOOLSETS, GITLAB_TOOLS, GITLAB_DENIED_TOOLS_REGEX, and tool-policy hidden/approve.
28
+ MCP compact (server): --compact-results replaces oversized tool replies with a preview plus a CLI command for the full payload.
29
+ `;
30
+ }
31
+ export function groupHelpText(group) {
32
+ const commands = listGroupActions(group);
33
+ if (commands.length === 0) {
34
+ return `Unknown group: ${group}\n`;
35
+ }
36
+ const lines = commands.map(command => ` ${command.group} ${command.action} ${command.tool}`);
37
+ return `${BINARY} ${group}
38
+
39
+ Usage:
40
+ ${BINARY} ${group} <action> [options]
41
+ ${BINARY} ${group} <action> --help
42
+
43
+ Actions:
44
+ ${lines.join("\n")}
45
+ `;
46
+ }
47
+ export function toolHelpText(toolName) {
48
+ const tool = allTools.find(entry => entry.name === toolName);
49
+ if (tool === undefined || toolName === "discover_tools") {
50
+ return `Unknown tool: ${toolName}\n`;
51
+ }
52
+ const schema = readObjectSchema(tool.inputSchema);
53
+ const description = typeof tool.description === "string" ? tool.description : "";
54
+ const flags = formatSchemaFlags(schema.properties, schema.required ?? []);
55
+ return `${BINARY} tool ${toolName}
56
+
57
+ ${description}
58
+
59
+ Flags:
60
+ ${flags}
61
+ --output json|table
62
+ --args-json '{...}'
63
+ `;
64
+ }
65
+ export function curatedHelpText(group, action) {
66
+ const command = CURATED_COMMANDS.find(entry => entry.group === group && entry.action === action);
67
+ if (command === undefined) {
68
+ return `Unknown command: ${group} ${action}\n`;
69
+ }
70
+ const tool = allTools.find(entry => entry.name === command.tool);
71
+ const schema = readObjectSchema(tool?.inputSchema);
72
+ const flags = formatSchemaFlags(schema.properties, schema.required ?? [], command.flagAliases);
73
+ return `${BINARY} ${group} ${action}
74
+
75
+ Maps to ${command.tool}${command.scope ? ` (or ${command.scope.groupTool} with --group-id)` : ""}
76
+
77
+ Flags:
78
+ ${flags}
79
+ --output table|json
80
+ --yes
81
+ `;
82
+ }
83
+ export function toolIndexHelpText() {
84
+ const names = allTools
85
+ .filter(tool => tool.name !== "discover_tools")
86
+ .map(tool => ` ${tool.name}`)
87
+ .join("\n");
88
+ return `${BINARY} tool <tool-name> [options]
89
+
90
+ Default output is JSON. Use --help after a tool name for generated flags.
91
+
92
+ Tools:
93
+ ${names}
94
+ `;
95
+ }
96
+ function formatSchemaFlags(properties, required, aliases) {
97
+ if (properties === undefined || Object.keys(properties).length === 0) {
98
+ return " (none)";
99
+ }
100
+ const requiredSet = new Set(required);
101
+ const aliasHints = aliases === undefined ? [] : Object.entries(aliases).map(([shortName, fullName]) => ` --${snakeToKebab(shortName)} alias for --${snakeToKebab(fullName)}`);
102
+ const flags = Object.entries(properties).map(([name, property]) => {
103
+ const suffix = requiredSet.has(name) ? " (required)" : "";
104
+ const description = typeof property.description === "string" ? ` ${property.description}` : "";
105
+ return ` --${snakeToKebab(name)}${suffix}${description}`;
106
+ });
107
+ return [...flags, ...aliasHints].join("\n");
108
+ }
@@ -0,0 +1,65 @@
1
+ import { renderTable } from "./tables.js";
2
+ export function formatToolOutput(input) {
3
+ const isError = resultIsError(input.result);
4
+ const text = unwrapToolText(input.result);
5
+ if (input.mode === "json") {
6
+ return { stdout: ensureTrailingNewline(jsonOrRaw(text)), stderr: "", isError };
7
+ }
8
+ const parsed = parseJsonValue(text);
9
+ if (parsed === undefined && input.tableSpec?.kind !== "text") {
10
+ return {
11
+ stdout: ensureTrailingNewline(text),
12
+ stderr: "No table renderer for this result; printed raw output.\n",
13
+ isError,
14
+ };
15
+ }
16
+ const rendered = renderTable(parsed ?? text, input.tableSpec);
17
+ return { stdout: rendered.stdout, stderr: rendered.stderr, isError };
18
+ }
19
+ export function formatCliError(error) {
20
+ const message = error instanceof Error ? error.message : String(error);
21
+ return `${redactSecrets(message)}\n`;
22
+ }
23
+ export function resultIsError(result) {
24
+ return isRecord(result) && result.isError === true;
25
+ }
26
+ function unwrapToolText(result) {
27
+ if (!isRecord(result) || !Array.isArray(result.content)) {
28
+ return stringifyUnknown(result);
29
+ }
30
+ const textBlock = result.content.find(item => isRecord(item) && typeof item.text === "string");
31
+ if (!isRecord(textBlock) || typeof textBlock.text !== "string") {
32
+ return stringifyUnknown(result);
33
+ }
34
+ return textBlock.text;
35
+ }
36
+ function jsonOrRaw(text) {
37
+ const parsed = parseJsonValue(text);
38
+ if (parsed === undefined) {
39
+ return JSON.stringify(text);
40
+ }
41
+ return JSON.stringify(parsed);
42
+ }
43
+ function parseJsonValue(text) {
44
+ try {
45
+ return JSON.parse(text);
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ function stringifyUnknown(value) {
52
+ if (typeof value === "string") {
53
+ return value;
54
+ }
55
+ return JSON.stringify(value);
56
+ }
57
+ function ensureTrailingNewline(text) {
58
+ return text.endsWith("\n") ? text : `${text}\n`;
59
+ }
60
+ function redactSecrets(message) {
61
+ return message.replace(/glpat-[A-Za-z0-9_-]+/g, "[REDACTED]");
62
+ }
63
+ function isRecord(value) {
64
+ return typeof value === "object" && value !== null && !Array.isArray(value);
65
+ }
@@ -0,0 +1,53 @@
1
+ import { snakeToKebab } from "./args.js";
2
+ const BINARY = "zereight-mcp-gitlab";
3
+ const SIMPLE_SHELL_TOKEN = /^[A-Za-z0-9._:/=+-]+$/;
4
+ const SECRET_ARGUMENT_NAMES = new Set([
5
+ "token",
6
+ "job_token",
7
+ "private_token",
8
+ "access_token",
9
+ "cookie",
10
+ "cookie_path",
11
+ "password",
12
+ "signing_token",
13
+ "authorization",
14
+ ]);
15
+ export function buildToolReplayCommand(toolName, args) {
16
+ const parts = [BINARY, "tool", toolName];
17
+ const nested = {};
18
+ if (args !== undefined) {
19
+ for (const key of Object.keys(args)) {
20
+ if (shouldOmitReplayArgument(key)) {
21
+ continue;
22
+ }
23
+ const value = Reflect.get(args, key);
24
+ if (value === undefined) {
25
+ continue;
26
+ }
27
+ if (isScalarReplayValue(value)) {
28
+ parts.push(`--${snakeToKebab(key)}`, shellToken(String(value)));
29
+ continue;
30
+ }
31
+ nested[key] = value;
32
+ }
33
+ }
34
+ if (Object.keys(nested).length > 0) {
35
+ parts.push("--args-json", shellToken(JSON.stringify(nested)));
36
+ }
37
+ return parts.join(" ");
38
+ }
39
+ function shouldOmitReplayArgument(name) {
40
+ if (name.startsWith("_")) {
41
+ return true;
42
+ }
43
+ return SECRET_ARGUMENT_NAMES.has(name);
44
+ }
45
+ function isScalarReplayValue(value) {
46
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
47
+ }
48
+ function shellToken(value) {
49
+ if (SIMPLE_SHELL_TOKEN.test(value)) {
50
+ return value;
51
+ }
52
+ return JSON.stringify(value);
53
+ }