@zereight/mcp-gitlab 2.2.1 → 2.2.2

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/build/auth-cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import * as os from "os";
2
2
  import * as path from "path";
3
+ import { parseCliArgs } from "./cli-boolean-flags.js";
4
+ import { deprecatedEnvInputFromSources, getDeprecatedEnvWarnings } from "./deprecated-env.js";
3
5
  import { GitLabOAuth } from "./oauth.js";
4
- import { createLogger } from "./utils/logger.js";
6
+ import { createLogger, flushStartupLogs } from "./utils/logger.js";
5
7
  const logger = createLogger("gitlab-mcp-auth");
6
8
  const DEFAULT_API_URL = "https://gitlab.com";
7
9
  const DEFAULT_REDIRECT_URI = "http://127.0.0.1:8888/callback";
@@ -42,12 +44,12 @@ function readFlag(argv, name) {
42
44
  function wantsHelp(argv) {
43
45
  return argv.includes("--help") || argv.includes("-h");
44
46
  }
45
- function isReadOnlyMode(argv, env) {
46
- const readOnly = readFlag(argv, "read-only") ?? env.GITLAB_READ_ONLY_MODE;
47
+ function isReadOnlyMode(parsedArgs, env) {
48
+ const readOnly = parsedArgs["read-only"] ?? env.GITLAB_READ_ONLY_MODE;
47
49
  if (readOnly === "true") {
48
50
  return true;
49
51
  }
50
- return (readFlag(argv, "permission-mode") ?? env.GITLAB_PERMISSION_MODE) === "readonly";
52
+ return (parsedArgs["permission-mode"] ?? env.GITLAB_PERMISSION_MODE) === "readonly";
51
53
  }
52
54
  export function gitlabOriginFromApiUrl(apiUrl) {
53
55
  return apiUrl.replace(/\/api\/v4\/?$/, "");
@@ -56,6 +58,14 @@ export async function runAuthCommandAsync(input = {}) {
56
58
  const argv = input.argv ?? process.argv;
57
59
  const env = input.env ?? process.env;
58
60
  const stdout = input.stdout ?? process.stdout;
61
+ const parsedArgs = parseCliArgs(argv.slice(2));
62
+ const warnings = getDeprecatedEnvWarnings(deprecatedEnvInputFromSources(parsedArgs, env));
63
+ for (const warning of warnings) {
64
+ logger.warn(warning);
65
+ }
66
+ if (warnings.length > 0) {
67
+ flushStartupLogs(logger);
68
+ }
59
69
  if (wantsHelp(argv)) {
60
70
  stdout.write(AUTH_CLI_HELP);
61
71
  return;
@@ -69,7 +79,7 @@ export async function runAuthCommandAsync(input = {}) {
69
79
  env.GITLAB_OAUTH_TOKEN_PATH ??
70
80
  path.join(os.homedir(), ".gitlab-mcp-token.json");
71
81
  const gitlabUrl = gitlabOriginFromApiUrl(apiUrl);
72
- const scopes = [isReadOnlyMode(argv, env) ? "read_api" : "api"];
82
+ const scopes = [isReadOnlyMode(parsedArgs, env) ? "read_api" : "api"];
73
83
  const oauth = new GitLabOAuth({
74
84
  clientId,
75
85
  clientSecret: env.GITLAB_OAUTH_CLIENT_SECRET,
@@ -36,3 +36,37 @@ export function nextBooleanFlagValue(nextToken) {
36
36
  }
37
37
  return { value: "true", consumeNext: false };
38
38
  }
39
+ /** Same flag parse as startup config: CLI values only, positionals ignored. */
40
+ export function parseCliArgs(argv) {
41
+ const parsed = {};
42
+ for (let index = 0; index < argv.length; index++) {
43
+ const arg = argv[index];
44
+ if (!arg.startsWith("--")) {
45
+ continue;
46
+ }
47
+ const eqIndex = arg.indexOf("=");
48
+ if (eqIndex !== -1) {
49
+ const key = arg.slice(2, eqIndex);
50
+ const value = arg.slice(eqIndex + 1);
51
+ if (value) {
52
+ parsed[key] = value;
53
+ }
54
+ continue;
55
+ }
56
+ const key = arg.slice(2);
57
+ if (BOOLEAN_CLI_FLAG_NAMES.has(key)) {
58
+ const next = nextBooleanFlagValue(argv[index + 1]);
59
+ parsed[key] = next.value;
60
+ if (next.consumeNext) {
61
+ index += 1;
62
+ }
63
+ continue;
64
+ }
65
+ const next = argv[index + 1];
66
+ if (next !== undefined && !next.startsWith("--")) {
67
+ index += 1;
68
+ parsed[key] = next;
69
+ }
70
+ }
71
+ return parsed;
72
+ }
package/build/config.js CHANGED
@@ -1,35 +1,9 @@
1
- import { BOOLEAN_CLI_FLAG_NAMES, nextBooleanFlagValue } from "./cli-boolean-flags.js";
1
+ import { parseCliArgs } from "./cli-boolean-flags.js";
2
+ import { deprecatedEnvInputFromSources } from "./deprecated-env.js";
2
3
  import { parseToolProfile } from "./tools/tool-profile.js";
3
4
  import { parseCompactToolAllowlist } from "./utils/compact-tool-allowlist.js";
4
5
  // Parse CLI arguments before anything else
5
- const args = process.argv.slice(2);
6
- export const cliArgs = {};
7
- for (let i = 0; i < args.length; i++) {
8
- const arg = args[i];
9
- if (arg.startsWith("--")) {
10
- const eqIndex = arg.indexOf("=");
11
- if (eqIndex !== -1) {
12
- const key = arg.slice(2, eqIndex);
13
- const value = arg.slice(eqIndex + 1);
14
- if (value) {
15
- cliArgs[key] = value;
16
- }
17
- }
18
- else {
19
- const key = arg.slice(2);
20
- if (BOOLEAN_CLI_FLAG_NAMES.has(key)) {
21
- const parsed = nextBooleanFlagValue(args[i + 1]);
22
- cliArgs[key] = parsed.value;
23
- if (parsed.consumeNext) {
24
- i += 1;
25
- }
26
- }
27
- else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
28
- cliArgs[key] = args[++i];
29
- }
30
- }
31
- }
32
- }
6
+ export const cliArgs = parseCliArgs(process.argv.slice(2));
33
7
  export function getConfig(cliKey, envKey, defaultValue) {
34
8
  return cliArgs[cliKey] || process.env[envKey] || defaultValue;
35
9
  }
@@ -103,7 +77,7 @@ export const GITLAB_OAUTH_SCOPES = GITLAB_OAUTH_SCOPES_RAW
103
77
  .filter(Boolean)
104
78
  : undefined;
105
79
  export const GITLAB_OAUTH_CALLBACK_PROXY = getConfig("oauth-callback-proxy", "GITLAB_OAUTH_CALLBACK_PROXY") === "true";
106
- /** @deprecated Use GITLAB_OAUTH_ALLOWED_GROUPS_RAW instead. Will be removed in the next major version. */
80
+ /** @deprecated Use GITLAB_OAUTH_ALLOWED_GROUPS_RAW instead. Will be removed in v3.0.0. */
107
81
  export const GITLAB_ALLOWED_GROUPS_RAW = getConfig("allowed-groups", "GITLAB_ALLOWED_GROUPS");
108
82
  export const GITLAB_OAUTH_ALLOWED_GROUPS_RAW = getConfig("oauth-allowed-groups", "GITLAB_OAUTH_ALLOWED_GROUPS");
109
83
  export const GITLAB_OAUTH_ALLOWED_GROUPS = (() => {
@@ -202,5 +176,13 @@ export const HTTPS_PROXY = getConfig("https-proxy", "HTTPS_PROXY");
202
176
  export const NO_PROXY = getConfig("no-proxy", "NO_PROXY");
203
177
  export const NODE_TLS_REJECT_UNAUTHORIZED = getConfig("tls-reject-unauthorized", "NODE_TLS_REJECT_UNAUTHORIZED");
204
178
  export const GITLAB_CA_CERT_PATH = getConfig("ca-cert-path", "GITLAB_CA_CERT_PATH");
179
+ /**
180
+ * Snapshot of deprecated settings for startup warnings.
181
+ * CLI flags win over env vars, matching getConfig, so the warning can name
182
+ * the flag the user actually passed.
183
+ */
184
+ export function buildDeprecatedEnvInput() {
185
+ return deprecatedEnvInputFromSources(cliArgs, process.env);
186
+ }
205
187
  const _poolMaxSizeRaw = getConfig("pool-max-size", "GITLAB_POOL_MAX_SIZE");
206
188
  export const GITLAB_POOL_MAX_SIZE = _poolMaxSizeRaw ? Number.parseInt(_poolMaxSizeRaw, 10) : 100;
@@ -5,31 +5,116 @@ function shownEnvValue(rawValue) {
5
5
  }
6
6
  return `${singleLine.slice(0, 77)}...`;
7
7
  }
8
+ const REMOVAL_VERSION = "v3.0.0";
9
+ const DEPRECATION_NOTICE_URL = "https://github.com/zereight/gitlab-mcp/issues/815";
8
10
  // USE_PIPELINE exposes these two tools, but they live in the `ci` toolset, not `pipelines`.
9
11
  // Suggesting `ci` would also turn on catalog tools the flag does not expose.
10
12
  const LEGACY_TOOLS_OUTSIDE_NAMED_TOOLSET = {
11
13
  pipelines: "validate_ci_lint,validate_project_ci_lint",
12
14
  };
13
- function legacyToolsetReplacement(toolset) {
14
- const toolsets = `GITLAB_TOOLSETS=core,${toolset}`;
15
+ function explicitToolsets(raw) {
16
+ if (raw === undefined) {
17
+ return undefined;
18
+ }
19
+ const trimmed = raw.trim();
20
+ if (trimmed === "") {
21
+ return undefined;
22
+ }
23
+ return trimmed;
24
+ }
25
+ function isAllToolsets(raw) {
26
+ return raw.trim().toLowerCase() === "all";
27
+ }
28
+ function settingName(envName, cliFlag, fromCli) {
29
+ if (fromCli) {
30
+ return `\`--${cliFlag}\` (${envName})`;
31
+ }
32
+ return envName;
33
+ }
34
+ function gitlabToolsAssignment(extraTools, toolsRaw, toolsFromCli) {
35
+ const prefix = toolsFromCli ? "--tools=" : "GITLAB_TOOLS=";
36
+ const existing = explicitToolsets(toolsRaw);
37
+ if (existing === undefined) {
38
+ return `${prefix}${extraTools}`;
39
+ }
40
+ const names = existing
41
+ .split(",")
42
+ .map(part => part.trim())
43
+ .filter(part => part.length > 0);
44
+ const merged = extraTools.split(",").reduce((current, tool) => {
45
+ if (current.includes(tool)) {
46
+ return current;
47
+ }
48
+ return [...current, tool];
49
+ }, names);
50
+ return `${prefix}${merged.join(",")}`;
51
+ }
52
+ function legacyToolsetHint(toolset, toolsetsRaw, toolsetsFromCli, toolsRaw, toolsFromCli, optional) {
15
53
  const extraTools = LEGACY_TOOLS_OUTSIDE_NAMED_TOOLSET[toolset];
16
- if (extraTools === undefined) {
17
- return toolsets;
54
+ const toolsAssignment = extraTools === undefined
55
+ ? undefined
56
+ : gitlabToolsAssignment(extraTools, toolsRaw, toolsFromCli);
57
+ const listed = explicitToolsets(toolsetsRaw);
58
+ const toolsetsLabel = toolsetsFromCli && listed !== undefined ? "--toolsets" : "GITLAB_TOOLSETS";
59
+ if (listed !== undefined) {
60
+ const shown = shownEnvValue(listed);
61
+ // parseEnabledToolsets treats only the exact value "all" as every toolset.
62
+ // "all,pipelines" is not "all"; unknown token "all" is dropped and pipelines remains.
63
+ if (isAllToolsets(listed)) {
64
+ return `Keep the existing ${toolsetsLabel} value ("${shown}") and remove the deprecated setting`;
65
+ }
66
+ const extra = toolsAssignment === undefined ? "" : ` and set ${toolsAssignment}`;
67
+ if (optional) {
68
+ return (`Add \`${toolset}\` to the existing ${toolsetsLabel} list ("${shown}")${extra} ` +
69
+ "only if you want those tools");
70
+ }
71
+ return `Add \`${toolset}\` to the existing ${toolsetsLabel} list ("${shown}")${extra} instead`;
72
+ }
73
+ // An explicit toolset list replaces the default toolsets; it does not merge.
74
+ // Legacy USE_* flags add tools on top of defaults, so the hint must keep `core`
75
+ // only when the user has not set a toolset list.
76
+ const toolsets = `GITLAB_TOOLSETS=core,${toolset}`;
77
+ const replacement = toolsAssignment === undefined ? toolsets : `${toolsets} and ${toolsAssignment}`;
78
+ if (optional) {
79
+ return `Set ${replacement} only if you want those tools in addition to core`;
18
80
  }
19
- return `${toolsets} and GITLAB_TOOLS=${extraTools}`;
81
+ return `Use ${replacement} instead`;
20
82
  }
21
- function legacyToolsetWarning(envName, rawValue, toolset) {
22
- // An explicit GITLAB_TOOLSETS list replaces the default toolsets; it does not merge.
23
- // Legacy USE_* flags add tools on top of defaults, so the hint must keep `core`.
24
- const replacement = legacyToolsetReplacement(toolset);
83
+ function legacyToolsetWarning(envName, cliFlag, fromCli, rawValue, toolset, toolsetsRaw, toolsetsFromCli, toolsRaw, toolsFromCli) {
84
+ const name = settingName(envName, cliFlag, fromCli);
25
85
  if (rawValue === "true") {
26
- return (`${envName} is deprecated and will be removed in the next major version. ` +
27
- `Use ${replacement} instead.`);
86
+ return (`${name} is deprecated and will be removed in ${REMOVAL_VERSION}. ` +
87
+ `${legacyToolsetHint(toolset, toolsetsRaw, toolsetsFromCli, toolsRaw, toolsFromCli, false)}.`);
28
88
  }
29
89
  const shown = shownEnvValue(rawValue);
30
- return (`${envName} is set to "${shown}" and is deprecated. ` +
90
+ return (`${name} is set to "${shown}" and is deprecated and will be removed in ${REMOVAL_VERSION}. ` +
31
91
  `This value does not enable the ${toolset} toolset. Remove it. ` +
32
- `Set ${replacement} only if you want those tools in addition to core.`);
92
+ `${legacyToolsetHint(toolset, toolsetsRaw, toolsetsFromCli, toolsRaw, toolsFromCli, true)}.`);
93
+ }
94
+ function readOnlyRawValue(input) {
95
+ if (input.readOnlyRaw !== undefined && input.readOnlyRaw !== "") {
96
+ return input.readOnlyRaw;
97
+ }
98
+ if (input.readOnlyMode) {
99
+ return "true";
100
+ }
101
+ return undefined;
102
+ }
103
+ function readOnlyWarning(input) {
104
+ const raw = readOnlyRawValue(input);
105
+ if (raw === undefined) {
106
+ return undefined;
107
+ }
108
+ const name = settingName("GITLAB_READ_ONLY_MODE", "read-only", input.readOnlyFromCli === true);
109
+ if (raw === "true") {
110
+ const conflict = input.permissionModeRaw !== undefined && input.permissionModeRaw !== "readonly"
111
+ ? ` It OVERRIDES the configured permission mode "${input.permissionModeRaw}"; the server runs as "readonly".`
112
+ : "";
113
+ return (`${name} is deprecated and will be removed in ${REMOVAL_VERSION}. ` +
114
+ `Use GITLAB_PERMISSION_MODE=readonly or --permission-mode=readonly instead.${conflict}`);
115
+ }
116
+ return (`${name} is set to "${shownEnvValue(raw)}" and is deprecated and will be removed in ${REMOVAL_VERSION}. ` +
117
+ "This value does not enable legacy read-only mode. The effective permission mode comes from GITLAB_PERMISSION_MODE or --permission-mode. Remove it.");
33
118
  }
34
119
  /**
35
120
  * Builds startup warnings for deprecated env vars / CLI flags.
@@ -37,27 +122,72 @@ function legacyToolsetWarning(envName, rawValue, toolset) {
37
122
  */
38
123
  export function getDeprecatedEnvWarnings(input) {
39
124
  const warnings = [];
40
- if (input.readOnlyMode) {
41
- const conflict = input.permissionModeRaw !== undefined && input.permissionModeRaw !== "readonly"
42
- ? ` It OVERRIDES the configured permission mode "${input.permissionModeRaw}"; the server runs as "readonly".`
43
- : "";
44
- warnings.push("GITLAB_READ_ONLY_MODE is deprecated and will be removed in the next major version. " +
45
- `Use GITLAB_PERMISSION_MODE=readonly or --permission-mode=readonly instead.${conflict}`);
125
+ const readOnly = readOnlyWarning(input);
126
+ if (readOnly !== undefined) {
127
+ warnings.push(readOnly);
46
128
  }
47
129
  if (input.allowedGroupsRaw) {
130
+ const name = settingName("GITLAB_ALLOWED_GROUPS", "allowed-groups", input.allowedGroupsFromCli === true);
48
131
  warnings.push(input.oauthAllowedGroupsRaw
49
- ? "GITLAB_ALLOWED_GROUPS is deprecated and ignored because GITLAB_OAUTH_ALLOWED_GROUPS is set. Remove it."
50
- : "GITLAB_ALLOWED_GROUPS is deprecated and will be removed in the next major version. Use GITLAB_OAUTH_ALLOWED_GROUPS instead.");
132
+ ? `${name} is deprecated and ignored because GITLAB_OAUTH_ALLOWED_GROUPS is set. Remove it. It will be removed in ${REMOVAL_VERSION}.`
133
+ : `${name} is deprecated and will be removed in ${REMOVAL_VERSION}. Use GITLAB_OAUTH_ALLOWED_GROUPS instead.`);
51
134
  }
52
135
  const legacyToolsetFlags = [
53
- [input.useWikiRaw, "USE_GITLAB_WIKI", "wiki"],
54
- [input.useMilestoneRaw, "USE_MILESTONE", "milestones"],
55
- [input.usePipelineRaw, "USE_PIPELINE", "pipelines"],
136
+ {
137
+ rawValue: input.useWikiRaw,
138
+ envName: "USE_GITLAB_WIKI",
139
+ cliFlag: "use-wiki",
140
+ fromCli: input.useWikiFromCli === true,
141
+ toolset: "wiki",
142
+ },
143
+ {
144
+ rawValue: input.useMilestoneRaw,
145
+ envName: "USE_MILESTONE",
146
+ cliFlag: "use-milestone",
147
+ fromCli: input.useMilestoneFromCli === true,
148
+ toolset: "milestones",
149
+ },
150
+ {
151
+ rawValue: input.usePipelineRaw,
152
+ envName: "USE_PIPELINE",
153
+ cliFlag: "use-pipeline",
154
+ fromCli: input.usePipelineFromCli === true,
155
+ toolset: "pipelines",
156
+ },
56
157
  ];
57
- for (const [rawValue, envName, toolset] of legacyToolsetFlags) {
58
- if (rawValue !== undefined) {
59
- warnings.push(legacyToolsetWarning(envName, rawValue, toolset));
158
+ for (const flag of legacyToolsetFlags) {
159
+ if (flag.rawValue !== undefined) {
160
+ warnings.push(legacyToolsetWarning(flag.envName, flag.cliFlag, flag.fromCli, flag.rawValue, flag.toolset, input.toolsetsRaw, input.toolsetsFromCli === true, input.toolsRaw, input.toolsFromCli === true));
60
161
  }
61
162
  }
62
- return warnings;
163
+ return warnings.map(warning => `${warning} See ${DEPRECATION_NOTICE_URL} for migration details.`);
164
+ }
165
+ function readSuppliedSetting(parsedArgs, env, cliKey, envKey) {
166
+ return parsedArgs[cliKey] || env[envKey];
167
+ }
168
+ /**
169
+ * Warning snapshot for one command. CLI flags in `parsedArgs` win over `env`,
170
+ * matching getConfig, and neither source is read from the process globals.
171
+ */
172
+ export function deprecatedEnvInputFromSources(parsedArgs, env) {
173
+ const readOnlyRaw = readSuppliedSetting(parsedArgs, env, "read-only", "GITLAB_READ_ONLY_MODE");
174
+ return {
175
+ readOnlyMode: readOnlyRaw === "true",
176
+ readOnlyRaw,
177
+ readOnlyFromCli: Boolean(parsedArgs["read-only"]),
178
+ permissionModeRaw: readSuppliedSetting(parsedArgs, env, "permission-mode", "GITLAB_PERMISSION_MODE"),
179
+ allowedGroupsRaw: readSuppliedSetting(parsedArgs, env, "allowed-groups", "GITLAB_ALLOWED_GROUPS"),
180
+ allowedGroupsFromCli: Boolean(parsedArgs["allowed-groups"]),
181
+ oauthAllowedGroupsRaw: readSuppliedSetting(parsedArgs, env, "oauth-allowed-groups", "GITLAB_OAUTH_ALLOWED_GROUPS"),
182
+ useWikiRaw: readSuppliedSetting(parsedArgs, env, "use-wiki", "USE_GITLAB_WIKI"),
183
+ useWikiFromCli: Boolean(parsedArgs["use-wiki"]),
184
+ useMilestoneRaw: readSuppliedSetting(parsedArgs, env, "use-milestone", "USE_MILESTONE"),
185
+ useMilestoneFromCli: Boolean(parsedArgs["use-milestone"]),
186
+ usePipelineRaw: readSuppliedSetting(parsedArgs, env, "use-pipeline", "USE_PIPELINE"),
187
+ usePipelineFromCli: Boolean(parsedArgs["use-pipeline"]),
188
+ toolsetsRaw: readSuppliedSetting(parsedArgs, env, "toolsets", "GITLAB_TOOLSETS"),
189
+ toolsetsFromCli: Boolean(parsedArgs["toolsets"]),
190
+ toolsRaw: readSuppliedSetting(parsedArgs, env, "tools", "GITLAB_TOOLS"),
191
+ toolsFromCli: Boolean(parsedArgs["tools"]),
192
+ };
63
193
  }