@zereight/mcp-gitlab 2.1.67 → 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 +8 -4
- package/README.md +14 -5
- package/README.pt-BR.md +589 -0
- package/README.zh-CN.md +8 -4
- 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 +329 -44
- package/build/oauth.js +93 -10
- package/build/schemas.js +187 -0
- package/build/scripts/generate-tool-docs.js +7 -1
- package/build/scripts/tool-coverage/coverage.js +2 -0
- 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 +162 -0
- package/build/test/test-geteffectiveprojectid.js +4 -2
- package/build/test/test-permission-mode.js +87 -0
- package/build/test/test-snippets.js +735 -0
- package/build/test/test-toolset-filtering.js +3 -0
- package/build/tools/denied-regex.js +38 -0
- package/build/tools/registry.js +42 -1
- package/package.json +1 -1
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
|
// ---------------------------------------------------------------------------
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_MASKING_ENABLED, GITLAB_MASKING_CONFIG, GITLAB_MASKING_POLICY_FILE, GITLAB_MASKING_WORKSPACE_DIR, GITLAB_PERMISSION_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, } from "./config.js";
|
|
2
|
+
import { getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_MASKING_ENABLED, GITLAB_MASKING_CONFIG, GITLAB_MASKING_POLICY_FILE, GITLAB_MASKING_WORKSPACE_DIR, GITLAB_PERMISSION_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_RESULT_CHARS, } from "./config.js";
|
|
3
3
|
/** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
|
|
4
4
|
const IS_REMOTE = SSE || STREAMABLE_HTTP;
|
|
5
5
|
const STREAMABLE_HTTP_AUTH_TOKEN = getConfig("streamable-http-auth-token", "STREAMABLE_HTTP_AUTH_TOKEN");
|
|
@@ -74,9 +74,13 @@ import path from "node:path";
|
|
|
74
74
|
import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
75
75
|
import { URL } from "node:url";
|
|
76
76
|
import { z } from "zod";
|
|
77
|
-
import {
|
|
77
|
+
import { createGitLabOAuthClient, ensureOAuthToken } from "./oauth.js";
|
|
78
78
|
import { getPositionalCliCommand } from "./cli-command.js";
|
|
79
79
|
import { runAuthCommandAsync } from "./auth-cli.js";
|
|
80
|
+
import { isCliInvocation, resolveCli } from "./cli/router.js";
|
|
81
|
+
import { formatCliError, formatToolOutput } from "./cli/output.js";
|
|
82
|
+
import { compactMcpToolResult, readOwnRecord } from "./cli/compact-mcp-result.js";
|
|
83
|
+
import { compileDeniedToolsRegex } from "./tools/denied-regex.js";
|
|
80
84
|
import { createGitLabOAuthProvider } from "./oauth-proxy.js";
|
|
81
85
|
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
82
86
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
|
@@ -114,7 +118,7 @@ GitLabDiscussionNoteSchema, // Added
|
|
|
114
118
|
GitLabDiscussionSchema,
|
|
115
119
|
// Draft Notes Schemas
|
|
116
120
|
GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabGroupMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineScheduleSchema, GitLabPipelineScheduleVariableSchema, GitLabPipelineSchedulePlayResultSchema, ListPipelineSchedulesSchema, GetPipelineScheduleSchema, ListPipelineSchedulePipelinesSchema, CreatePipelineScheduleSchema, UpdatePipelineScheduleSchema, DeletePipelineScheduleSchema, PlayPipelineScheduleSchema, TakeOwnershipPipelineScheduleSchema, GetPipelineScheduleVariableSchema, CreatePipelineScheduleVariableSchema, UpdatePipelineScheduleVariableSchema, DeletePipelineScheduleVariableSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, GitLabCiVariableSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GitLabDependencyProxySchema, GitLabDependencyProxyBlobSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
117
|
-
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, GetMergeRequestDiscussionSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, CreateWebhookSchema, UpdateWebhookSchema, DeleteWebhookSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
|
|
121
|
+
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, ListSnippetsSchema, GetSnippetSchema, CreateSnippetSchema, UpdateSnippetSchema, DeleteSnippetSchema, GitLabSnippetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, GetMergeRequestDiscussionSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, CreateWebhookSchema, UpdateWebhookSchema, DeleteWebhookSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
|
|
118
122
|
import { randomUUID, timingSafeEqual, } from "node:crypto";
|
|
119
123
|
import { createLogger } from "./utils/logger.js";
|
|
120
124
|
const logger = createLogger();
|
|
@@ -179,6 +183,7 @@ const MCP_PROMPTS = [
|
|
|
179
183
|
* Each transport connection gets its own Server instance to prevent
|
|
180
184
|
* cross-client data leakage (GHSA-345p-7cg4-v4c7).
|
|
181
185
|
*/
|
|
186
|
+
/** Builds an MCP server with the tool list filtered by toolsets, permission mode, and policy. */
|
|
182
187
|
function createServer() {
|
|
183
188
|
// Precompute filtered tool list once at server creation (Steps 1–5 are static)
|
|
184
189
|
// Step 1: Toolset filter — keep tools in enabled toolsets
|
|
@@ -329,11 +334,18 @@ function createServer() {
|
|
|
329
334
|
? (value) => maskingEngine.maskValue(value)
|
|
330
335
|
: undefined,
|
|
331
336
|
});
|
|
332
|
-
|
|
337
|
+
const masked = maskingEngine
|
|
333
338
|
? maskingEngine.maskToolResult(filtered, {
|
|
334
339
|
textFormat: isPlainTextResult ? "plain" : "json",
|
|
335
340
|
})
|
|
336
341
|
: filtered;
|
|
342
|
+
return compactMcpToolResult({
|
|
343
|
+
result: masked,
|
|
344
|
+
enabled: GITLAB_MCP_COMPACT_RESULTS,
|
|
345
|
+
maxChars: GITLAB_MCP_COMPACT_RESULT_CHARS,
|
|
346
|
+
toolName,
|
|
347
|
+
args: readOwnRecord(request.params.arguments),
|
|
348
|
+
});
|
|
337
349
|
};
|
|
338
350
|
const logError = (error) => {
|
|
339
351
|
const durationMs = Date.now() - start;
|
|
@@ -398,6 +410,16 @@ function createServer() {
|
|
|
398
410
|
if (jmespathSyntaxError) {
|
|
399
411
|
return logCompletion({ content: [{ type: "text", text: jmespathSyntaxError }], isError: true }, { skipJmespath: true });
|
|
400
412
|
}
|
|
413
|
+
// Enforce list-time filtering on direct calls: clients can call names that tools/list omits.
|
|
414
|
+
// filteredTools is extended by discover_tools; hidden-policy tools stay callable by design,
|
|
415
|
+
// but GITLAB_DENIED_TOOLS_REGEX always wins over the hidden exemption.
|
|
416
|
+
// Permission-mode denials fall through so handleToolCall reports its mode-specific error.
|
|
417
|
+
const hiddenAndNotDenied = hiddenToolSet.has(toolName) && !GITLAB_DENIED_TOOLS_REGEX?.test(toolName);
|
|
418
|
+
if (isToolAllowedByPermissionMode(toolName) &&
|
|
419
|
+
!hiddenAndNotDenied &&
|
|
420
|
+
!filteredTools.some(t => t.name === toolName)) {
|
|
421
|
+
throw new Error(`Tool "${toolName}" is not available on this server`);
|
|
422
|
+
}
|
|
401
423
|
// Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
|
|
402
424
|
if (toolName === "discover_tools") {
|
|
403
425
|
const category = request.params.arguments?.category?.trim()?.toLowerCase();
|
|
@@ -526,6 +548,7 @@ function createServer() {
|
|
|
526
548
|
}
|
|
527
549
|
catch (error) {
|
|
528
550
|
logError(error);
|
|
551
|
+
throw error;
|
|
529
552
|
}
|
|
530
553
|
});
|
|
531
554
|
return mcpServer;
|
|
@@ -846,15 +869,10 @@ export function hasStatelessSessionId(req) {
|
|
|
846
869
|
* This avoids background timers that cause issues with multiple instances.
|
|
847
870
|
*/
|
|
848
871
|
async function ensureValidOAuthToken() {
|
|
849
|
-
if (!oauthClient)
|
|
850
|
-
return;
|
|
851
|
-
if (oauthClient.hasValidToken())
|
|
852
|
-
return;
|
|
853
872
|
try {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
logger.info("OAuth token refreshed successfully");
|
|
873
|
+
await ensureOAuthToken(oauthClient, OAUTH_ACCESS_TOKEN, token => {
|
|
874
|
+
OAUTH_ACCESS_TOKEN = token;
|
|
875
|
+
});
|
|
858
876
|
}
|
|
859
877
|
catch (error) {
|
|
860
878
|
logger.error({ err: error }, "Failed to refresh OAuth token");
|
|
@@ -873,33 +891,11 @@ function isToolAllowedByPermissionMode(toolName) {
|
|
|
873
891
|
}
|
|
874
892
|
}
|
|
875
893
|
const GITLAB_DENIED_TOOLS_REGEX = (() => {
|
|
876
|
-
const
|
|
877
|
-
if (
|
|
878
|
-
|
|
879
|
-
// Reject patterns that are too long (potential ReDoS vector)
|
|
880
|
-
const MAX_PATTERN_LENGTH = 200;
|
|
881
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
882
|
-
logger.error(`GITLAB_DENIED_TOOLS_REGEX pattern exceeds ${MAX_PATTERN_LENGTH} chars. Ignoring.`);
|
|
883
|
-
return undefined;
|
|
884
|
-
}
|
|
885
|
-
// Reject patterns with nested quantifiers that can cause catastrophic backtracking (ReDoS)
|
|
886
|
-
// e.g., (a+)+, (a*)+, (a+)*, (a{1,})+
|
|
887
|
-
// Note: lookahead (?!), (?=), lookbehind (?<), and named groups (?<name>) are safe and allowed
|
|
888
|
-
const NESTED_QUANTIFIER_PATTERN = /(\(.*[+*?].*\)|\[.*\])[+*?]/;
|
|
889
|
-
if (NESTED_QUANTIFIER_PATTERN.test(pattern)) {
|
|
890
|
-
logger.error(`GITLAB_DENIED_TOOLS_REGEX contains potentially unsafe nested quantifiers. Ignoring.`);
|
|
891
|
-
return undefined;
|
|
892
|
-
}
|
|
893
|
-
try {
|
|
894
|
-
const regex = new RegExp(pattern);
|
|
895
|
-
// Dry-run against a sample string to catch immediate issues
|
|
896
|
-
regex.test("sample_tool_name");
|
|
897
|
-
return regex;
|
|
898
|
-
}
|
|
899
|
-
catch {
|
|
900
|
-
logger.error(`Invalid GITLAB_DENIED_TOOLS_REGEX pattern: "${pattern}". Ignoring.`);
|
|
901
|
-
return undefined;
|
|
894
|
+
const compiled = compileDeniedToolsRegex(getConfig("denied-tools-regex", "GITLAB_DENIED_TOOLS_REGEX"));
|
|
895
|
+
if (compiled.error) {
|
|
896
|
+
logger.error(compiled.error);
|
|
902
897
|
}
|
|
898
|
+
return compiled.regex;
|
|
903
899
|
})();
|
|
904
900
|
// ---------------------------------------------------------------------------
|
|
905
901
|
// Tool policy: approve / hidden sets
|
|
@@ -1317,7 +1313,7 @@ if (GITLAB_MCP_OAUTH) {
|
|
|
1317
1313
|
}
|
|
1318
1314
|
logger.info("MCP OAuth enabled: GitLab OAuth proxy active (Private-Token/JOB-TOKEN headers bypass OAuth)");
|
|
1319
1315
|
}
|
|
1320
|
-
if (
|
|
1316
|
+
if (!isCliInvocation(process.argv) &&
|
|
1321
1317
|
!REMOTE_AUTHORIZATION &&
|
|
1322
1318
|
!GITLAB_MCP_OAUTH &&
|
|
1323
1319
|
!USE_OAUTH &&
|
|
@@ -7605,6 +7601,163 @@ async function executeGitLabGraphQL(query, variables = {}) {
|
|
|
7605
7601
|
clearTimeout(timeout);
|
|
7606
7602
|
}
|
|
7607
7603
|
}
|
|
7604
|
+
/**
|
|
7605
|
+
* Build the snippets endpoint URL.
|
|
7606
|
+
*
|
|
7607
|
+
* When projectId is provided, or when a project allowlist is in effect (env
|
|
7608
|
+
* GITLAB_PROJECT_ID / GITLAB_ALLOWED_PROJECT_IDS or a session scope narrowed via
|
|
7609
|
+
* X-GitLab-Allowed-Project-Ids), returns the project snippets endpoint — routing
|
|
7610
|
+
* through getEffectiveProjectId so the effective scope is enforced. Only falls
|
|
7611
|
+
* back to the personal /snippets endpoint when no scope is active at all.
|
|
7612
|
+
*/
|
|
7613
|
+
function getSnippetsEndpoint(projectId) {
|
|
7614
|
+
const scopeActive = getEffectiveAllowedProjectIds().length > 0 || !!GITLAB_PROJECT_ID;
|
|
7615
|
+
if (projectId || scopeActive) {
|
|
7616
|
+
const decoded = projectId ? decodeURIComponent(projectId) : "";
|
|
7617
|
+
const effectiveProjectId = getEffectiveProjectId(decoded);
|
|
7618
|
+
return `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/snippets`;
|
|
7619
|
+
}
|
|
7620
|
+
return `${getEffectiveApiUrl()}/snippets`;
|
|
7621
|
+
}
|
|
7622
|
+
/**
|
|
7623
|
+
* List snippets — project snippets if projectId is given, otherwise personal snippets.
|
|
7624
|
+
*/
|
|
7625
|
+
async function listSnippets(projectId, options = {}) {
|
|
7626
|
+
const url = new URL(getSnippetsEndpoint(projectId));
|
|
7627
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
7628
|
+
if (value !== undefined) {
|
|
7629
|
+
url.searchParams.append(key, String(value));
|
|
7630
|
+
}
|
|
7631
|
+
});
|
|
7632
|
+
const response = await fetch(url.toString(), {
|
|
7633
|
+
...getFetchConfig(),
|
|
7634
|
+
});
|
|
7635
|
+
await handleGitLabError(response);
|
|
7636
|
+
const data = await response.json();
|
|
7637
|
+
return GitLabSnippetSchema.array().parse(data);
|
|
7638
|
+
}
|
|
7639
|
+
/**
|
|
7640
|
+
* Get a snippet's metadata. Use getSnippetRawContent to fetch the raw file content.
|
|
7641
|
+
*/
|
|
7642
|
+
async function getSnippet(projectId, snippetId) {
|
|
7643
|
+
const response = await fetch(`${getSnippetsEndpoint(projectId)}/${snippetId}`, {
|
|
7644
|
+
...getFetchConfig(),
|
|
7645
|
+
});
|
|
7646
|
+
await handleGitLabError(response);
|
|
7647
|
+
const data = await response.json();
|
|
7648
|
+
return GitLabSnippetSchema.parse(data);
|
|
7649
|
+
}
|
|
7650
|
+
/**
|
|
7651
|
+
* Get the raw content of a single-file snippet via the `/raw` endpoint.
|
|
7652
|
+
* For multi-file snippets, use getSnippetFileRawContent instead.
|
|
7653
|
+
*/
|
|
7654
|
+
async function getSnippetRawContent(projectId, snippetId) {
|
|
7655
|
+
const response = await fetch(`${getSnippetsEndpoint(projectId)}/${snippetId}/raw`, {
|
|
7656
|
+
...getFetchConfig(),
|
|
7657
|
+
});
|
|
7658
|
+
await handleGitLabError(response);
|
|
7659
|
+
return await response.text();
|
|
7660
|
+
}
|
|
7661
|
+
/**
|
|
7662
|
+
* Extract the ref (branch/tag/commit) from a snippet file's raw_url.
|
|
7663
|
+
* Anchors on /snippets/{id}/raw/ so branch names or file paths containing
|
|
7664
|
+
* the word "raw" don't produce a false match.
|
|
7665
|
+
*/
|
|
7666
|
+
function extractSnippetRef(rawUrl, snippetId, filePath) {
|
|
7667
|
+
const rawMarker = `/snippets/${snippetId}/raw/`;
|
|
7668
|
+
const decoded = decodeURIComponent(new URL(rawUrl).pathname);
|
|
7669
|
+
const markerIdx = decoded.indexOf(rawMarker);
|
|
7670
|
+
if (markerIdx === -1) {
|
|
7671
|
+
throw new Error(`Cannot extract ref from snippet file raw_url: ${rawUrl}`);
|
|
7672
|
+
}
|
|
7673
|
+
const afterRaw = decoded.slice(markerIdx + rawMarker.length); // "{ref}/{filePath}"
|
|
7674
|
+
const fileStart = afterRaw.lastIndexOf("/" + filePath);
|
|
7675
|
+
if (fileStart === -1) {
|
|
7676
|
+
throw new Error(`Cannot locate file path "${filePath}" in snippet file raw_url: ${rawUrl}`);
|
|
7677
|
+
}
|
|
7678
|
+
return afterRaw.slice(0, fileStart);
|
|
7679
|
+
}
|
|
7680
|
+
/**
|
|
7681
|
+
* Fetch the raw content of one file inside a multi-file snippet.
|
|
7682
|
+
* Accepts an explicit ref (branch/tag/commit) — callers resolve it via
|
|
7683
|
+
* extractSnippetRef or a user-supplied parameter before calling this.
|
|
7684
|
+
*/
|
|
7685
|
+
async function getSnippetFileRawContent(projectId, snippetId, ref, filePath) {
|
|
7686
|
+
const encodedRef = encodeGitLabPathSegment(ref);
|
|
7687
|
+
const encodedPath = encodeGitLabPathSegment(filePath);
|
|
7688
|
+
const url = `${getSnippetsEndpoint(projectId)}/${snippetId}/files/${encodedRef}/${encodedPath}/raw`;
|
|
7689
|
+
const response = await fetch(url, { ...getFetchConfig() });
|
|
7690
|
+
await handleGitLabError(response);
|
|
7691
|
+
return await response.text();
|
|
7692
|
+
}
|
|
7693
|
+
function snippetCreateFilesPayload(options) {
|
|
7694
|
+
if (options.files !== undefined && options.files.length > 0) {
|
|
7695
|
+
return options.files;
|
|
7696
|
+
}
|
|
7697
|
+
if (options.file_name !== undefined && options.content !== undefined) {
|
|
7698
|
+
return [{ file_path: options.file_name, content: options.content }];
|
|
7699
|
+
}
|
|
7700
|
+
throw new Error("Provide either files[] (multi-file) or both file_name and content (single-file)");
|
|
7701
|
+
}
|
|
7702
|
+
/**
|
|
7703
|
+
* Create a snippet — project-scoped if projectId is given, otherwise personal.
|
|
7704
|
+
*/
|
|
7705
|
+
async function createSnippet(projectId, options) {
|
|
7706
|
+
const { title, description, visibility } = options;
|
|
7707
|
+
const body = {
|
|
7708
|
+
title,
|
|
7709
|
+
visibility: visibility ?? "private",
|
|
7710
|
+
files: snippetCreateFilesPayload(options),
|
|
7711
|
+
};
|
|
7712
|
+
if (description !== undefined) {
|
|
7713
|
+
body.description = description;
|
|
7714
|
+
}
|
|
7715
|
+
const response = await fetch(getSnippetsEndpoint(projectId), {
|
|
7716
|
+
...getFetchConfig(),
|
|
7717
|
+
method: "POST",
|
|
7718
|
+
body: JSON.stringify(body),
|
|
7719
|
+
});
|
|
7720
|
+
await handleGitLabError(response);
|
|
7721
|
+
const data = await response.json();
|
|
7722
|
+
return GitLabSnippetSchema.parse(data);
|
|
7723
|
+
}
|
|
7724
|
+
/**
|
|
7725
|
+
* Update an existing snippet — project-scoped if projectId is given, otherwise personal.
|
|
7726
|
+
*/
|
|
7727
|
+
async function updateSnippet(projectId, options) {
|
|
7728
|
+
const { snippet_id, title, file_name, content, files, description, visibility } = options;
|
|
7729
|
+
const body = {};
|
|
7730
|
+
if (title !== undefined)
|
|
7731
|
+
body.title = title;
|
|
7732
|
+
if (description !== undefined)
|
|
7733
|
+
body.description = description;
|
|
7734
|
+
if (visibility !== undefined)
|
|
7735
|
+
body.visibility = visibility;
|
|
7736
|
+
if (files !== undefined) {
|
|
7737
|
+
body.files = files;
|
|
7738
|
+
}
|
|
7739
|
+
else if (file_name !== undefined && content !== undefined) {
|
|
7740
|
+
body.files = [{ action: "update", file_path: file_name, content }];
|
|
7741
|
+
}
|
|
7742
|
+
const response = await fetch(`${getSnippetsEndpoint(projectId)}/${snippet_id}`, {
|
|
7743
|
+
...getFetchConfig(),
|
|
7744
|
+
method: "PUT",
|
|
7745
|
+
body: JSON.stringify(body),
|
|
7746
|
+
});
|
|
7747
|
+
await handleGitLabError(response);
|
|
7748
|
+
const data = await response.json();
|
|
7749
|
+
return GitLabSnippetSchema.parse(data);
|
|
7750
|
+
}
|
|
7751
|
+
/**
|
|
7752
|
+
* Delete a snippet — project-scoped if projectId is given, otherwise personal.
|
|
7753
|
+
*/
|
|
7754
|
+
async function deleteSnippet(projectId, snippetId) {
|
|
7755
|
+
const response = await fetch(`${getSnippetsEndpoint(projectId)}/${snippetId}`, {
|
|
7756
|
+
...getFetchConfig(),
|
|
7757
|
+
method: "DELETE",
|
|
7758
|
+
});
|
|
7759
|
+
await handleGitLabError(response);
|
|
7760
|
+
}
|
|
7608
7761
|
// Request handlers are now registered inside createServer() factory function
|
|
7609
7762
|
// to ensure each transport connection gets its own Server instance (GHSA-345p-7cg4-v4c7).
|
|
7610
7763
|
async function handleToolCall(params) {
|
|
@@ -10129,6 +10282,82 @@ async function handleToolCall(params) {
|
|
|
10129
10282
|
content: [{ type: "text", text: JSON.stringify(signature) }],
|
|
10130
10283
|
};
|
|
10131
10284
|
}
|
|
10285
|
+
case "list_snippets": {
|
|
10286
|
+
const args = ListSnippetsSchema.parse(params.arguments);
|
|
10287
|
+
const { project_id, ...options } = args;
|
|
10288
|
+
const snippets = await listSnippets(project_id, options);
|
|
10289
|
+
return {
|
|
10290
|
+
content: [{ type: "text", text: JSON.stringify(snippets, null, 2) }],
|
|
10291
|
+
};
|
|
10292
|
+
}
|
|
10293
|
+
case "get_snippet": {
|
|
10294
|
+
const args = GetSnippetSchema.parse(params.arguments);
|
|
10295
|
+
const snippet = await getSnippet(args.project_id, args.snippet_id);
|
|
10296
|
+
const result = { ...snippet };
|
|
10297
|
+
if (args.include_content) {
|
|
10298
|
+
const files = snippet.files ?? [];
|
|
10299
|
+
if (files.length > 1) {
|
|
10300
|
+
const firstFile = files[0];
|
|
10301
|
+
let ref;
|
|
10302
|
+
if (args.ref !== undefined) {
|
|
10303
|
+
ref = args.ref;
|
|
10304
|
+
}
|
|
10305
|
+
else {
|
|
10306
|
+
if (!firstFile.raw_url)
|
|
10307
|
+
throw new Error(`Snippet file "${firstFile.path}" has no raw_url`);
|
|
10308
|
+
ref = extractSnippetRef(firstFile.raw_url, args.snippet_id, firstFile.path);
|
|
10309
|
+
}
|
|
10310
|
+
result.files = await Promise.all(files.map(async (f) => ({
|
|
10311
|
+
...f,
|
|
10312
|
+
content: await getSnippetFileRawContent(args.project_id, args.snippet_id, ref, f.path),
|
|
10313
|
+
})));
|
|
10314
|
+
}
|
|
10315
|
+
else {
|
|
10316
|
+
const file = files[0];
|
|
10317
|
+
const filePath = file?.path ?? snippet.file_name ?? undefined;
|
|
10318
|
+
if (args.ref !== undefined && filePath) {
|
|
10319
|
+
result.content = await getSnippetFileRawContent(args.project_id, args.snippet_id, args.ref, filePath);
|
|
10320
|
+
}
|
|
10321
|
+
else {
|
|
10322
|
+
result.content = await getSnippetRawContent(args.project_id, args.snippet_id);
|
|
10323
|
+
}
|
|
10324
|
+
}
|
|
10325
|
+
}
|
|
10326
|
+
return {
|
|
10327
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
10328
|
+
};
|
|
10329
|
+
}
|
|
10330
|
+
case "create_snippet": {
|
|
10331
|
+
const args = CreateSnippetSchema.parse(params.arguments);
|
|
10332
|
+
const { project_id, ...options } = args;
|
|
10333
|
+
const snippet = await createSnippet(project_id, options);
|
|
10334
|
+
return {
|
|
10335
|
+
content: [{ type: "text", text: JSON.stringify(snippet, null, 2) }],
|
|
10336
|
+
};
|
|
10337
|
+
}
|
|
10338
|
+
case "update_snippet": {
|
|
10339
|
+
const args = UpdateSnippetSchema.parse(params.arguments);
|
|
10340
|
+
const { project_id, ...options } = args;
|
|
10341
|
+
const snippet = await updateSnippet(project_id, options);
|
|
10342
|
+
return {
|
|
10343
|
+
content: [{ type: "text", text: JSON.stringify(snippet, null, 2) }],
|
|
10344
|
+
};
|
|
10345
|
+
}
|
|
10346
|
+
case "delete_snippet": {
|
|
10347
|
+
const args = DeleteSnippetSchema.parse(params.arguments);
|
|
10348
|
+
await deleteSnippet(args.project_id, args.snippet_id);
|
|
10349
|
+
return {
|
|
10350
|
+
content: [
|
|
10351
|
+
{
|
|
10352
|
+
type: "text",
|
|
10353
|
+
text: JSON.stringify({
|
|
10354
|
+
status: "success",
|
|
10355
|
+
message: `Snippet ${args.snippet_id} deleted successfully`,
|
|
10356
|
+
}, null, 2),
|
|
10357
|
+
},
|
|
10358
|
+
],
|
|
10359
|
+
};
|
|
10360
|
+
}
|
|
10132
10361
|
case "list_webhooks": {
|
|
10133
10362
|
const args = ListWebhooksSchema.parse(params.arguments);
|
|
10134
10363
|
const webhooks = await listWebhooks(args);
|
|
@@ -11771,10 +12000,12 @@ async function runServer() {
|
|
|
11771
12000
|
logger.info("Using OAuth authentication...");
|
|
11772
12001
|
try {
|
|
11773
12002
|
const gitlabBaseUrl = GITLAB_API_URL.replace(/\/api\/v4$/, "");
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
12003
|
+
// Construct the client synchronously (no network). The token is
|
|
12004
|
+
// acquired lazily on the first tool call via ensureValidOAuthToken,
|
|
12005
|
+
// by which point the network is ready. This avoids blocking startup
|
|
12006
|
+
// and avoids opening a browser on transient boot-time network errors.
|
|
12007
|
+
oauthClient = createGitLabOAuthClient(gitlabBaseUrl);
|
|
12008
|
+
logger.info("OAuth enabled; token acquired lazily on first use.");
|
|
11778
12009
|
}
|
|
11779
12010
|
catch (error) {
|
|
11780
12011
|
logger.error({ err: error }, "OAuth authentication failed");
|
|
@@ -11816,7 +12047,16 @@ async function runServer() {
|
|
|
11816
12047
|
}
|
|
11817
12048
|
}
|
|
11818
12049
|
async function main() {
|
|
11819
|
-
|
|
12050
|
+
const cli = resolveCli(process.argv);
|
|
12051
|
+
if (cli.kind === "help") {
|
|
12052
|
+
process.stdout.write(cli.text);
|
|
12053
|
+
process.exit(0);
|
|
12054
|
+
}
|
|
12055
|
+
if (cli.kind === "usage" || cli.kind === "refused") {
|
|
12056
|
+
process.stderr.write(`${cli.message}\n`);
|
|
12057
|
+
process.exit(2);
|
|
12058
|
+
}
|
|
12059
|
+
if (cli.kind === "auth" || getPositionalCliCommand(process.argv) === "auth") {
|
|
11820
12060
|
try {
|
|
11821
12061
|
await runAuthCommandAsync();
|
|
11822
12062
|
process.exit(0);
|
|
@@ -11828,8 +12068,53 @@ async function main() {
|
|
|
11828
12068
|
process.exit(1);
|
|
11829
12069
|
}
|
|
11830
12070
|
}
|
|
12071
|
+
if (cli.kind === "run") {
|
|
12072
|
+
if (!hasCliCredentials()) {
|
|
12073
|
+
process.stderr.write("Missing GitLab credentials. Set --token or GITLAB_PERSONAL_ACCESS_TOKEN, or run `auth` with GITLAB_USE_OAUTH=true.\n");
|
|
12074
|
+
process.exit(2);
|
|
12075
|
+
}
|
|
12076
|
+
try {
|
|
12077
|
+
const result = await handleToolCall({ name: cli.toolName, arguments: cli.args });
|
|
12078
|
+
const masked = maskCliToolResult(cli.toolName, result);
|
|
12079
|
+
const formatted = formatToolOutput({
|
|
12080
|
+
result: masked,
|
|
12081
|
+
mode: cli.output,
|
|
12082
|
+
tableSpec: cli.tableSpec,
|
|
12083
|
+
});
|
|
12084
|
+
if (formatted.stderr) {
|
|
12085
|
+
process.stderr.write(formatted.stderr);
|
|
12086
|
+
}
|
|
12087
|
+
process.stdout.write(formatted.stdout);
|
|
12088
|
+
process.exit(formatted.isError ? 1 : 0);
|
|
12089
|
+
}
|
|
12090
|
+
catch (error) {
|
|
12091
|
+
process.stderr.write(formatCliError(error));
|
|
12092
|
+
logger.error({ err: error }, "cli command failed");
|
|
12093
|
+
process.exit(1);
|
|
12094
|
+
}
|
|
12095
|
+
}
|
|
11831
12096
|
await runServer();
|
|
11832
12097
|
}
|
|
12098
|
+
function hasCliCredentials() {
|
|
12099
|
+
return Boolean(GITLAB_PERSONAL_ACCESS_TOKEN || GITLAB_JOB_TOKEN || GITLAB_AUTH_COOKIE_PATH || USE_OAUTH);
|
|
12100
|
+
}
|
|
12101
|
+
function isRecord(value) {
|
|
12102
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12103
|
+
}
|
|
12104
|
+
function maskCliToolResult(toolName, result) {
|
|
12105
|
+
const gitlabInstance = getConfig("api-url", "GITLAB_API_URL") || "https://gitlab.com";
|
|
12106
|
+
const engine = maskingPolicyResolver?.select({
|
|
12107
|
+
gitlabInstance,
|
|
12108
|
+
allowBuiltinForUnscoped: true,
|
|
12109
|
+
});
|
|
12110
|
+
if (!engine || !isRecord(result)) {
|
|
12111
|
+
return result;
|
|
12112
|
+
}
|
|
12113
|
+
const isPlainText = toolName === "get_pipeline_job_output" ||
|
|
12114
|
+
toolName === "get_job_artifact_file" ||
|
|
12115
|
+
(toolName === "download_release_asset" && !IS_REMOTE);
|
|
12116
|
+
return engine.maskToolResult(result, { textFormat: isPlainText ? "plain" : "json" });
|
|
12117
|
+
}
|
|
11833
12118
|
main().catch(error => {
|
|
11834
12119
|
logger.fatal({ err: error }, "Fatal error in main()");
|
|
11835
12120
|
process.exit(1);
|