@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.
- package/README.ko.md +11 -3
- package/README.md +237 -232
- package/README.pt-BR.md +8 -4
- package/README.zh-CN.md +7 -3
- 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 +176 -90
- 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/credentials.test.js +153 -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/gitlab-api-tool-errors.test.js +112 -0
- package/build/test/oauth-device-flow-tests.js +10 -0
- package/build/test/oauth-download-url.test.js +157 -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 +20 -26
- package/build/test/test-permission-mode.js +86 -0
- package/build/test/test-pipeline-schedules.js +3 -1
- package/build/test/utils/url.test.js +14 -1
- package/build/tools/denied-regex.js +38 -0
- package/build/utils/url.js +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { GITLAB_PERMISSION_MODE } from "../config.js";
|
|
2
|
+
import { allTools, deleteTools, destructiveTools, readOnlyTools } from "../tools/registry.js";
|
|
3
|
+
import { getPositionalCliCommand } from "../cli-command.js";
|
|
4
|
+
import { applyFlagAliases, argvHasHelp, CliUsageError, parseArgv, parseToolArgs, } from "./args.js";
|
|
5
|
+
import { liveCliExposure } from "./exposure.js";
|
|
6
|
+
import { canonicalGroup, findCuratedCommand, isKnownCliCommand, resolveScopedTool, } from "./curated.js";
|
|
7
|
+
import { curatedHelpText, globalHelpText, groupHelpText, toolHelpText, toolIndexHelpText, } from "./help.js";
|
|
8
|
+
export function isCliInvocation(argv) {
|
|
9
|
+
if (argvHasHelp(argv)) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
const positional = getPositionalCliCommand(argv);
|
|
13
|
+
if (positional === undefined) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return isKnownCliCommand(positional);
|
|
17
|
+
}
|
|
18
|
+
export function resolveCli(argv, options = {}) {
|
|
19
|
+
try {
|
|
20
|
+
return resolveCliUnsafe(argv, options.permissionMode ?? GITLAB_PERMISSION_MODE, options.exposure ?? liveCliExposure());
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof CliUsageError) {
|
|
24
|
+
return { kind: "usage", message: error.message };
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function resolveCliUnsafe(argv, permissionMode, exposure) {
|
|
30
|
+
const parsed = parseArgv(argv);
|
|
31
|
+
const [head, action, extra] = parsed.positionals;
|
|
32
|
+
if (head === undefined) {
|
|
33
|
+
if (parsed.help) {
|
|
34
|
+
return { kind: "help", text: globalHelpText() };
|
|
35
|
+
}
|
|
36
|
+
return { kind: "server" };
|
|
37
|
+
}
|
|
38
|
+
if (head === "auth") {
|
|
39
|
+
return { kind: "auth" };
|
|
40
|
+
}
|
|
41
|
+
if (head === "tool") {
|
|
42
|
+
if (extra !== undefined) {
|
|
43
|
+
return { kind: "usage", message: `Unexpected extra arguments: ${parsed.positionals.slice(2).join(" ")}` };
|
|
44
|
+
}
|
|
45
|
+
if (action === undefined) {
|
|
46
|
+
if (parsed.help) {
|
|
47
|
+
return { kind: "help", text: toolIndexHelpText() };
|
|
48
|
+
}
|
|
49
|
+
return { kind: "usage", message: `Usage: zereight-mcp-gitlab tool <tool-name> [options]` };
|
|
50
|
+
}
|
|
51
|
+
if (parsed.help) {
|
|
52
|
+
return { kind: "help", text: toolHelpText(action) };
|
|
53
|
+
}
|
|
54
|
+
return buildRun({
|
|
55
|
+
toolName: action,
|
|
56
|
+
flags: parsed.toolFlags,
|
|
57
|
+
argsJson: parsed.argsJson,
|
|
58
|
+
extraArgs: undefined,
|
|
59
|
+
output: parsed.output ?? "json",
|
|
60
|
+
yes: parsed.yes,
|
|
61
|
+
permissionMode,
|
|
62
|
+
exposure,
|
|
63
|
+
tableSpec: undefined,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const group = canonicalGroup(head);
|
|
67
|
+
if (group === undefined) {
|
|
68
|
+
if (parsed.help) {
|
|
69
|
+
return { kind: "help", text: globalHelpText() };
|
|
70
|
+
}
|
|
71
|
+
return { kind: "server" };
|
|
72
|
+
}
|
|
73
|
+
if (extra !== undefined) {
|
|
74
|
+
return { kind: "usage", message: `Unexpected extra arguments: ${parsed.positionals.slice(2).join(" ")}` };
|
|
75
|
+
}
|
|
76
|
+
if (action === undefined) {
|
|
77
|
+
return { kind: "help", text: groupHelpText(group) };
|
|
78
|
+
}
|
|
79
|
+
if (parsed.help) {
|
|
80
|
+
return { kind: "help", text: curatedHelpText(group, action) };
|
|
81
|
+
}
|
|
82
|
+
const command = findCuratedCommand(group, action);
|
|
83
|
+
if (command === undefined) {
|
|
84
|
+
return { kind: "usage", message: `Unknown command: ${head} ${action}` };
|
|
85
|
+
}
|
|
86
|
+
const aliased = applyFlagAliases(parsed.toolFlags, command.flagAliases);
|
|
87
|
+
const scopeArgs = {
|
|
88
|
+
...(parsed.argsJson ?? {}),
|
|
89
|
+
...aliased,
|
|
90
|
+
...(command.extraArgs ?? {}),
|
|
91
|
+
};
|
|
92
|
+
const toolName = resolveScopedTool(command, scopeArgs);
|
|
93
|
+
return buildRun({
|
|
94
|
+
toolName,
|
|
95
|
+
flags: aliased,
|
|
96
|
+
argsJson: parsed.argsJson,
|
|
97
|
+
extraArgs: command.extraArgs,
|
|
98
|
+
output: parsed.output ?? "table",
|
|
99
|
+
yes: parsed.yes,
|
|
100
|
+
permissionMode,
|
|
101
|
+
exposure,
|
|
102
|
+
tableSpec: command.table,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function buildRun(input) {
|
|
106
|
+
if (input.toolName === "discover_tools") {
|
|
107
|
+
return {
|
|
108
|
+
kind: "usage",
|
|
109
|
+
message: "discover_tools is MCP-only; use `tool <name>` for registry tools allowed by the current filters",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const tool = allTools.find(entry => entry.name === input.toolName);
|
|
113
|
+
if (tool === undefined) {
|
|
114
|
+
return { kind: "usage", message: `Unknown tool: ${input.toolName}` };
|
|
115
|
+
}
|
|
116
|
+
const args = parseToolArgs({
|
|
117
|
+
schema: tool.inputSchema,
|
|
118
|
+
flags: input.flags,
|
|
119
|
+
argsJson: input.argsJson,
|
|
120
|
+
extraArgs: input.extraArgs,
|
|
121
|
+
});
|
|
122
|
+
const exposureMessage = input.exposure.exposureRefusal(input.toolName);
|
|
123
|
+
if (exposureMessage !== undefined) {
|
|
124
|
+
return { kind: "refused", message: exposureMessage };
|
|
125
|
+
}
|
|
126
|
+
const permissionMessage = permissionRefusal(input.toolName, input.permissionMode);
|
|
127
|
+
if (permissionMessage !== undefined) {
|
|
128
|
+
return { kind: "refused", message: permissionMessage };
|
|
129
|
+
}
|
|
130
|
+
if (destructiveTools.has(input.toolName) && !input.yes) {
|
|
131
|
+
return {
|
|
132
|
+
kind: "refused",
|
|
133
|
+
message: `Would run ${input.toolName} with arguments [${Object.keys(args).join(", ")}]. Re-run with --yes to proceed.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (input.exposure.needsConfirmation(input.toolName) && !input.yes) {
|
|
137
|
+
return {
|
|
138
|
+
kind: "refused",
|
|
139
|
+
message: `${input.toolName} requires confirmation (GITLAB_TOOL_POLICY_APPROVE). Re-run with --yes to proceed.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
kind: "run",
|
|
144
|
+
toolName: input.toolName,
|
|
145
|
+
args,
|
|
146
|
+
output: input.output,
|
|
147
|
+
tableSpec: input.tableSpec,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function permissionRefusal(toolName, permissionMode) {
|
|
151
|
+
if (permissionMode === "readonly" && !readOnlyTools.has(toolName)) {
|
|
152
|
+
return `${toolName} is not allowed in read-only mode`;
|
|
153
|
+
}
|
|
154
|
+
if (permissionMode === "modify" && deleteTools.has(toolName)) {
|
|
155
|
+
return `${toolName} is not allowed in modify mode (delete operations are disabled)`;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const MAX_CELL = 48;
|
|
2
|
+
export function renderTable(value, spec) {
|
|
3
|
+
if (spec === undefined || spec.kind === "text") {
|
|
4
|
+
return { stdout: stringifyValue(value), stderr: "" };
|
|
5
|
+
}
|
|
6
|
+
const rows = spec.kind === "list" ? asObjectRows(value) : undefined;
|
|
7
|
+
if (spec.kind === "list") {
|
|
8
|
+
if (rows === undefined) {
|
|
9
|
+
return {
|
|
10
|
+
stdout: stringifyValue(value),
|
|
11
|
+
stderr: "No table renderer for this result; printed JSON.\n",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (rows.length === 0) {
|
|
15
|
+
return { stdout: "No items.\n", stderr: "" };
|
|
16
|
+
}
|
|
17
|
+
const columns = spec.columns ?? inferColumns(rows);
|
|
18
|
+
return { stdout: formatGrid(rows, columns), stderr: "" };
|
|
19
|
+
}
|
|
20
|
+
const record = asRecord(value);
|
|
21
|
+
if (record === undefined) {
|
|
22
|
+
return {
|
|
23
|
+
stdout: stringifyValue(value),
|
|
24
|
+
stderr: "No table renderer for this result; printed JSON.\n",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const keys = spec.columns ?? Object.keys(record);
|
|
28
|
+
return { stdout: formatKeyValue(record, keys), stderr: "" };
|
|
29
|
+
}
|
|
30
|
+
function asObjectRows(value) {
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value.every(isRecord) ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
const record = asRecord(value);
|
|
35
|
+
if (record === undefined) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
for (const nested of Object.values(record)) {
|
|
39
|
+
if (Array.isArray(nested) && nested.every(isRecord)) {
|
|
40
|
+
return nested;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function inferColumns(rows) {
|
|
46
|
+
const preferred = ["iid", "id", "name", "key", "tag_name", "slug", "short_id", "state", "status", "title", "path_with_namespace"];
|
|
47
|
+
const present = new Set();
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
for (const key of Object.keys(row)) {
|
|
50
|
+
present.add(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const columns = preferred.filter(key => present.has(key));
|
|
54
|
+
return columns.length > 0 ? columns : Object.keys(rows[0] ?? {}).slice(0, 4);
|
|
55
|
+
}
|
|
56
|
+
function formatGrid(rows, columns) {
|
|
57
|
+
const header = columns.map(key => key.toUpperCase());
|
|
58
|
+
const cells = rows.map(row => columns.map(key => cellText(row[key])));
|
|
59
|
+
const widths = columns.map((_, index) => {
|
|
60
|
+
const longest = cells.reduce((max, row) => Math.max(max, row[index]?.length ?? 0), header[index]?.length ?? 0);
|
|
61
|
+
return Math.min(longest, MAX_CELL);
|
|
62
|
+
});
|
|
63
|
+
const lines = [
|
|
64
|
+
padRow(header, widths),
|
|
65
|
+
...cells.map(row => padRow(row, widths)),
|
|
66
|
+
];
|
|
67
|
+
return `${lines.join("\n")}\n`;
|
|
68
|
+
}
|
|
69
|
+
function formatKeyValue(record, keys) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
for (const key of keys) {
|
|
72
|
+
if (record[key] === undefined) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
lines.push(`${key}\t${cellText(record[key])}`);
|
|
76
|
+
}
|
|
77
|
+
if (lines.length === 0) {
|
|
78
|
+
return stringifyValue(record);
|
|
79
|
+
}
|
|
80
|
+
return `${lines.join("\n")}\n`;
|
|
81
|
+
}
|
|
82
|
+
function padRow(cells, widths) {
|
|
83
|
+
return cells
|
|
84
|
+
.map((cell, index) => truncate(cell).padEnd(widths[index] ?? 0))
|
|
85
|
+
.join(" ")
|
|
86
|
+
.trimEnd();
|
|
87
|
+
}
|
|
88
|
+
function cellText(value) {
|
|
89
|
+
if (value === null || value === undefined) {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
93
|
+
return truncate(String(value));
|
|
94
|
+
}
|
|
95
|
+
return truncate(JSON.stringify(value));
|
|
96
|
+
}
|
|
97
|
+
function truncate(value) {
|
|
98
|
+
if (value.length <= MAX_CELL) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
return `${value.slice(0, MAX_CELL - 1)}…`;
|
|
102
|
+
}
|
|
103
|
+
function stringifyValue(value) {
|
|
104
|
+
if (typeof value === "string") {
|
|
105
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
106
|
+
}
|
|
107
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
108
|
+
}
|
|
109
|
+
function asRecord(value) {
|
|
110
|
+
if (!isRecord(value)) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function isRecord(value) {
|
|
116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Boolean flags accept `--flag`, `--flag=true`, or `--flag false`.
|
|
2
|
+
// They must not consume the following positional command
|
|
3
|
+
// (`--use-oauth mr list` is `mr`, not `list`).
|
|
4
|
+
export const BOOLEAN_CLI_FLAG_NAMES = new Set([
|
|
5
|
+
"use-oauth",
|
|
6
|
+
"is-old",
|
|
7
|
+
"read-only",
|
|
8
|
+
"masking-enabled",
|
|
9
|
+
"use-wiki",
|
|
10
|
+
"use-milestone",
|
|
11
|
+
"use-pipeline",
|
|
12
|
+
"disable-version-check",
|
|
13
|
+
"sse",
|
|
14
|
+
"streamable-http",
|
|
15
|
+
"remote-auth",
|
|
16
|
+
"mcp-oauth",
|
|
17
|
+
"allow-unauthenticated-tool-discovery",
|
|
18
|
+
"mcp-trust-proxy",
|
|
19
|
+
"oauth-callback-proxy",
|
|
20
|
+
"enable-dynamic-api-url",
|
|
21
|
+
"enable-dynamic-project-scope",
|
|
22
|
+
"enable-strict-project-scope",
|
|
23
|
+
"oauth-stateless-mode",
|
|
24
|
+
"compact-results",
|
|
25
|
+
]);
|
|
26
|
+
const BOOLEAN_FLAG_LITERALS = new Set(["true", "false", "1", "0"]);
|
|
27
|
+
export function isBooleanFlagLiteral(value) {
|
|
28
|
+
if (value === undefined || value.startsWith("-")) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return BOOLEAN_FLAG_LITERALS.has(value.toLowerCase());
|
|
32
|
+
}
|
|
33
|
+
export function nextBooleanFlagValue(nextToken) {
|
|
34
|
+
if (isBooleanFlagLiteral(nextToken)) {
|
|
35
|
+
return { value: nextToken, consumeNext: true };
|
|
36
|
+
}
|
|
37
|
+
return { value: "true", consumeNext: false };
|
|
38
|
+
}
|
package/build/cli-command.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { BOOLEAN_CLI_FLAG_NAMES, isBooleanFlagLiteral } from "./cli-boolean-flags.js";
|
|
1
2
|
const SCRIPT_PATH_PATTERN = /\.(cjs|mjs|js|cts|mts|ts)$/;
|
|
2
|
-
const FLAGS_WITHOUT_VALUE = new Set([
|
|
3
|
+
const FLAGS_WITHOUT_VALUE = new Set([
|
|
4
|
+
"--help",
|
|
5
|
+
"-h",
|
|
6
|
+
"--yes",
|
|
7
|
+
...[...BOOLEAN_CLI_FLAG_NAMES].map(name => `--${name}`),
|
|
8
|
+
]);
|
|
3
9
|
function isScriptPath(arg) {
|
|
4
10
|
return SCRIPT_PATH_PATTERN.test(arg);
|
|
5
11
|
}
|
|
@@ -17,9 +23,13 @@ export function getPositionalCliCommand(argv) {
|
|
|
17
23
|
continue;
|
|
18
24
|
}
|
|
19
25
|
if (arg.startsWith("-")) {
|
|
20
|
-
if (
|
|
21
|
-
i + 1
|
|
22
|
-
|
|
26
|
+
if (FLAGS_WITHOUT_VALUE.has(arg)) {
|
|
27
|
+
if (BOOLEAN_CLI_FLAG_NAMES.has(arg.replace(/^--/, "")) && isBooleanFlagLiteral(args[i + 1])) {
|
|
28
|
+
i += 1;
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
23
33
|
i += 1;
|
|
24
34
|
}
|
|
25
35
|
continue;
|
package/build/config.js
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
|
+
import { BOOLEAN_CLI_FLAG_NAMES, nextBooleanFlagValue } from "./cli-boolean-flags.js";
|
|
1
2
|
// Parse CLI arguments before anything else
|
|
2
3
|
const args = process.argv.slice(2);
|
|
3
4
|
export const cliArgs = {};
|
|
4
5
|
for (let i = 0; i < args.length; i++) {
|
|
5
6
|
const arg = args[i];
|
|
6
7
|
if (arg.startsWith("--")) {
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
|
|
8
|
+
const eqIndex = arg.indexOf("=");
|
|
9
|
+
if (eqIndex !== -1) {
|
|
10
|
+
const key = arg.slice(2, eqIndex);
|
|
11
|
+
const value = arg.slice(eqIndex + 1);
|
|
12
|
+
if (value) {
|
|
13
|
+
cliArgs[key] = value;
|
|
14
|
+
}
|
|
10
15
|
}
|
|
11
|
-
else
|
|
12
|
-
|
|
16
|
+
else {
|
|
17
|
+
const key = arg.slice(2);
|
|
18
|
+
if (BOOLEAN_CLI_FLAG_NAMES.has(key)) {
|
|
19
|
+
const parsed = nextBooleanFlagValue(args[i + 1]);
|
|
20
|
+
cliArgs[key] = parsed.value;
|
|
21
|
+
if (parsed.consumeNext) {
|
|
22
|
+
i += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
26
|
+
cliArgs[key] = args[++i];
|
|
27
|
+
}
|
|
13
28
|
}
|
|
14
29
|
}
|
|
15
30
|
}
|
|
@@ -76,7 +91,9 @@ export const MCP_SERVER_URL = getConfig("mcp-server-url", "MCP_SERVER_URL");
|
|
|
76
91
|
export const GITLAB_OAUTH_APP_ID = getConfig("oauth-app-id", "GITLAB_OAUTH_APP_ID");
|
|
77
92
|
export const GITLAB_OAUTH_SCOPES_RAW = getConfig("oauth-scopes", "GITLAB_OAUTH_SCOPES");
|
|
78
93
|
export const GITLAB_OAUTH_SCOPES = GITLAB_OAUTH_SCOPES_RAW
|
|
79
|
-
? GITLAB_OAUTH_SCOPES_RAW.split(",")
|
|
94
|
+
? GITLAB_OAUTH_SCOPES_RAW.split(",")
|
|
95
|
+
.map(s => s.trim())
|
|
96
|
+
.filter(Boolean)
|
|
80
97
|
: undefined;
|
|
81
98
|
export const GITLAB_OAUTH_CALLBACK_PROXY = getConfig("oauth-callback-proxy", "GITLAB_OAUTH_CALLBACK_PROXY") === "true";
|
|
82
99
|
/** @deprecated Use GITLAB_OAUTH_ALLOWED_GROUPS_RAW instead. Will be removed in the next major version. */
|
|
@@ -88,7 +105,10 @@ export const GITLAB_OAUTH_ALLOWED_GROUPS = (() => {
|
|
|
88
105
|
const raw = newVar ?? oldVar;
|
|
89
106
|
if (!raw)
|
|
90
107
|
return undefined;
|
|
91
|
-
const groups = raw
|
|
108
|
+
const groups = raw
|
|
109
|
+
.split(",")
|
|
110
|
+
.map(g => g.trim())
|
|
111
|
+
.filter(Boolean);
|
|
92
112
|
return groups.length > 0 ? groups : undefined;
|
|
93
113
|
})();
|
|
94
114
|
export const ENABLE_DYNAMIC_API_URL = getConfig("enable-dynamic-api-url", "ENABLE_DYNAMIC_API_URL") === "true";
|
|
@@ -163,6 +183,9 @@ export const HOST = getConfig("host", "HOST") || "127.0.0.1";
|
|
|
163
183
|
/** Default HTTP port for the MCP server. */
|
|
164
184
|
const _PORT_DEFAULT = 3002;
|
|
165
185
|
export const PORT = _intEnv("PORT", "port", _PORT_DEFAULT);
|
|
186
|
+
/** When true, oversized MCP tool replies are replaced with a preview plus a CLI replay command. */
|
|
187
|
+
export const GITLAB_MCP_COMPACT_RESULTS = getConfig("compact-results", "GITLAB_MCP_COMPACT_RESULTS") === "true";
|
|
188
|
+
export const GITLAB_MCP_COMPACT_RESULT_CHARS = _intEnv("GITLAB_MCP_COMPACT_RESULT_CHARS", "compact-result-chars", 4000);
|
|
166
189
|
// ---------------------------------------------------------------------------
|
|
167
190
|
// Proxy configuration
|
|
168
191
|
// ---------------------------------------------------------------------------
|