@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/README.ko.md +20 -13
- package/README.md +80 -67
- package/README.pt-BR.md +19 -12
- package/README.zh-CN.md +19 -12
- package/build/auth-cli.js +15 -5
- package/build/cli-boolean-flags.js +34 -0
- package/build/config.js +12 -30
- package/build/deprecated-env.js +158 -28
- package/build/index.js +328 -42
- package/build/oauth-proxy.js +2 -1
- package/build/oauth.js +5 -4
- package/build/schemas.js +97 -0
- package/build/tools/registry.js +54 -3
- package/build/tools/tool-descriptions.js +5 -1
- package/build/utils/health-check-transport-error.js +115 -0
- package/build/utils/logger.js +92 -16
- package/build/utils/redact-sensitive.js +18 -8
- package/build/utils/redact-url.js +141 -0
- package/package.json +1 -1
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,
|
|
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_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, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_RESULT_CHARS, GITLAB_MCP_COMPACT_TOOL_NAMES, GITLAB_TOOL_PROFILE, buildDeprecatedEnvInput, } from "./config.js";
|
|
3
3
|
import { getDeprecatedEnvWarnings } from "./deprecated-env.js";
|
|
4
4
|
/** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
|
|
5
5
|
const IS_REMOTE = SSE || STREAMABLE_HTTP;
|
|
@@ -97,6 +97,8 @@ import { graphqlQueryContainsWriteOperation, graphqlQueryContainsDeleteOperation
|
|
|
97
97
|
import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
|
|
98
98
|
import { encodeRepoFilePayloadContent, fileOperationsIncludeDeleteOrMove, toGitLabCommitActions, } from "./utils/gitlab-commit-actions.js";
|
|
99
99
|
import { redactSensitiveGitLabFields } from "./utils/redact-sensitive.js";
|
|
100
|
+
import { classifyHealthCheckTransportError, redactGitLabUrlCredentials, } from "./utils/health-check-transport-error.js";
|
|
101
|
+
import { redactUrlSecretsInText } from "./utils/redact-url.js";
|
|
100
102
|
import { createMaskingPolicyResolver, getManagedMaskingProjectIds, } from "./masking/index.js";
|
|
101
103
|
import { checkForNewVersion } from "./utils/version-check.js";
|
|
102
104
|
import { assertGitLabVersionAtLeast } from "./utils/gitlab-version-gate.js";
|
|
@@ -119,11 +121,15 @@ CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMerg
|
|
|
119
121
|
GitLabDiscussionNoteSchema, // Added
|
|
120
122
|
GitLabDiscussionSchema,
|
|
121
123
|
// Draft Notes Schemas
|
|
122
|
-
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
|
|
123
|
-
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";
|
|
124
|
+
GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabProtectedTagSchema, 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
|
|
125
|
+
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListProjectRunnersSchema, GetRunnerSchema, 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, ListProtectedTagsSchema, GetProtectedTagSchema, ProtectTagSchema, UnprotectTagSchema, 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";
|
|
124
126
|
import { randomUUID, timingSafeEqual, } from "node:crypto";
|
|
125
|
-
import { createLogger } from "./utils/logger.js";
|
|
127
|
+
import { createLogger, flushBufferedLogs, flushStartupLogs } from "./utils/logger.js";
|
|
126
128
|
const logger = createLogger();
|
|
129
|
+
function exitAfterFlush(code) {
|
|
130
|
+
flushBufferedLogs(logger);
|
|
131
|
+
process.exit(code);
|
|
132
|
+
}
|
|
127
133
|
// Construct once at startup. When disabled this is undefined and the result
|
|
128
134
|
// path below returns the original objects without reading a config file.
|
|
129
135
|
const maskingPolicyResolver = createMaskingPolicyResolver({
|
|
@@ -303,6 +309,7 @@ function createServer() {
|
|
|
303
309
|
durationMs,
|
|
304
310
|
error: safeError instanceof Error ? safeError.message : String(safeError),
|
|
305
311
|
}, `tool_call_error: ${toolName} (${durationMs}ms)`);
|
|
312
|
+
flushBufferedLogs(logger);
|
|
306
313
|
return safeError;
|
|
307
314
|
};
|
|
308
315
|
try {
|
|
@@ -502,7 +509,9 @@ function createServer() {
|
|
|
502
509
|
return logCompletion(result);
|
|
503
510
|
}
|
|
504
511
|
catch (error) {
|
|
505
|
-
const isRecoverableToolError = error instanceof GitLabApiError ||
|
|
512
|
+
const isRecoverableToolError = error instanceof GitLabApiError ||
|
|
513
|
+
error instanceof ToolInputError ||
|
|
514
|
+
error instanceof ProjectAccessDeniedError;
|
|
506
515
|
const safeError = logError(error);
|
|
507
516
|
if (isRecoverableToolError) {
|
|
508
517
|
return {
|
|
@@ -709,7 +718,7 @@ function validateConfiguration() {
|
|
|
709
718
|
new URL(url.trim());
|
|
710
719
|
}
|
|
711
720
|
catch {
|
|
712
|
-
errors.push(`GITLAB_API_URL contains an invalid URL: ${url.trim()}`);
|
|
721
|
+
errors.push(`GITLAB_API_URL contains an invalid URL: ${redactUrlSecretsInText(url.trim())}`);
|
|
713
722
|
}
|
|
714
723
|
}
|
|
715
724
|
}
|
|
@@ -795,7 +804,7 @@ function validateConfiguration() {
|
|
|
795
804
|
if (errors.length > 0) {
|
|
796
805
|
logger.error("Configuration validation failed:");
|
|
797
806
|
errors.forEach(err => logger.error(` - ${err}`));
|
|
798
|
-
|
|
807
|
+
exitAfterFlush(1);
|
|
799
808
|
}
|
|
800
809
|
logger.info("Configuration validation passed");
|
|
801
810
|
}
|
|
@@ -807,6 +816,20 @@ let oauthClient = null;
|
|
|
807
816
|
* switch to signed/sealed opaque values instead of per-pod in-memory caches.
|
|
808
817
|
*/
|
|
809
818
|
let STATELESS_MATERIAL = null;
|
|
819
|
+
let hasEmittedDeprecatedEnvWarnings = false;
|
|
820
|
+
function emitDeprecatedEnvWarnings() {
|
|
821
|
+
if (hasEmittedDeprecatedEnvWarnings) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
hasEmittedDeprecatedEnvWarnings = true;
|
|
825
|
+
const warnings = getDeprecatedEnvWarnings(buildDeprecatedEnvInput());
|
|
826
|
+
for (const warning of warnings) {
|
|
827
|
+
logger.warn(warning);
|
|
828
|
+
}
|
|
829
|
+
if (warnings.length > 0) {
|
|
830
|
+
flushStartupLogs(logger);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
810
833
|
try {
|
|
811
834
|
// Drive enablement from the already-resolved config flag so the CLI flag
|
|
812
835
|
// (--oauth-stateless-mode) is honored. Re-reading env.OAUTH_STATELESS_MODE
|
|
@@ -821,6 +844,7 @@ try {
|
|
|
821
844
|
}
|
|
822
845
|
}
|
|
823
846
|
catch (err) {
|
|
847
|
+
emitDeprecatedEnvWarnings();
|
|
824
848
|
// eslint-disable-next-line no-console -- startup failure must be visible
|
|
825
849
|
console.error(`[gitlab-mcp] failed to load stateless secret: ${err.message}`);
|
|
826
850
|
process.exit(1);
|
|
@@ -1064,7 +1088,11 @@ const BASE_HEADERS = {
|
|
|
1064
1088
|
function buildAuthHeaders() {
|
|
1065
1089
|
if (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) {
|
|
1066
1090
|
const ctx = sessionAuthStore.getStore();
|
|
1067
|
-
logger.debug({
|
|
1091
|
+
logger.debug({
|
|
1092
|
+
sessionId: ctx?.sessionId,
|
|
1093
|
+
header: ctx?.header,
|
|
1094
|
+
apiUrl: ctx?.apiUrl === undefined ? undefined : redactUrlSecretsInText(ctx.apiUrl),
|
|
1095
|
+
}, "buildAuthHeaders: session context");
|
|
1068
1096
|
if (ctx?.token) {
|
|
1069
1097
|
return {
|
|
1070
1098
|
[ctx.header]: ctx.header === "Authorization" ? `Bearer ${ctx.token}` : ctx.token,
|
|
@@ -1112,7 +1140,11 @@ function getEffectiveApiUrl() {
|
|
|
1112
1140
|
if (ctx?.apiUrl) {
|
|
1113
1141
|
return ctx.apiUrl;
|
|
1114
1142
|
}
|
|
1115
|
-
logger.warn({
|
|
1143
|
+
logger.warn({
|
|
1144
|
+
sessionId: ctx?.sessionId,
|
|
1145
|
+
header: ctx?.header,
|
|
1146
|
+
apiUrl: ctx?.apiUrl === undefined ? undefined : redactUrlSecretsInText(ctx.apiUrl),
|
|
1147
|
+
}, "getEffectiveApiUrl: No context or apiUrl found, falling back to default");
|
|
1116
1148
|
}
|
|
1117
1149
|
return GITLAB_API_URL;
|
|
1118
1150
|
}
|
|
@@ -1278,27 +1310,31 @@ const GITLAB_REPO_FILE_ENCODING = getConfig("repo-file-encoding", "GITLAB_REPO_F
|
|
|
1278
1310
|
if (REMOTE_AUTHORIZATION) {
|
|
1279
1311
|
// Remote authorization mode: token comes from HTTP headers
|
|
1280
1312
|
if (SSE) {
|
|
1313
|
+
emitDeprecatedEnvWarnings();
|
|
1281
1314
|
logger.error("REMOTE_AUTHORIZATION=true is not compatible with SSE transport mode");
|
|
1282
1315
|
logger.error("Please use STREAMABLE_HTTP=true instead");
|
|
1283
|
-
|
|
1316
|
+
exitAfterFlush(1);
|
|
1284
1317
|
}
|
|
1285
1318
|
if (!STREAMABLE_HTTP) {
|
|
1319
|
+
emitDeprecatedEnvWarnings();
|
|
1286
1320
|
logger.error("REMOTE_AUTHORIZATION=true requires STREAMABLE_HTTP=true");
|
|
1287
1321
|
logger.error("Set STREAMABLE_HTTP=true to enable remote authorization");
|
|
1288
|
-
|
|
1322
|
+
exitAfterFlush(1);
|
|
1289
1323
|
}
|
|
1290
1324
|
logger.info("Remote authorization enabled: tokens will be read from HTTP headers");
|
|
1291
1325
|
}
|
|
1292
1326
|
if (GITLAB_MCP_OAUTH) {
|
|
1293
1327
|
if (SSE) {
|
|
1328
|
+
emitDeprecatedEnvWarnings();
|
|
1294
1329
|
logger.error("GITLAB_MCP_OAUTH=true is not compatible with SSE transport mode");
|
|
1295
1330
|
logger.error("Please use STREAMABLE_HTTP=true instead");
|
|
1296
|
-
|
|
1331
|
+
exitAfterFlush(1);
|
|
1297
1332
|
}
|
|
1298
1333
|
if (!STREAMABLE_HTTP) {
|
|
1334
|
+
emitDeprecatedEnvWarnings();
|
|
1299
1335
|
logger.error("GITLAB_MCP_OAUTH=true requires STREAMABLE_HTTP=true");
|
|
1300
1336
|
logger.error("Set STREAMABLE_HTTP=true to enable MCP OAuth");
|
|
1301
|
-
|
|
1337
|
+
exitAfterFlush(1);
|
|
1302
1338
|
}
|
|
1303
1339
|
logger.info("MCP OAuth enabled: GitLab OAuth proxy active (Private-Token/JOB-TOKEN headers bypass OAuth)");
|
|
1304
1340
|
}
|
|
@@ -1309,10 +1345,12 @@ if (!isCliInvocation(process.argv) &&
|
|
|
1309
1345
|
!GITLAB_PERSONAL_ACCESS_TOKEN &&
|
|
1310
1346
|
!GITLAB_JOB_TOKEN &&
|
|
1311
1347
|
!GITLAB_AUTH_COOKIE_PATH) {
|
|
1312
|
-
// Standard mode: token must be in environment (unless using OAuth)
|
|
1348
|
+
// Standard mode: token must be in environment (unless using OAuth).
|
|
1349
|
+
// This exit runs before runServer(), so warnings have to be printed here too.
|
|
1350
|
+
emitDeprecatedEnvWarnings();
|
|
1313
1351
|
logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
|
|
1314
1352
|
logger.info("Either set GITLAB_PERSONAL_ACCESS_TOKEN or enable OAuth with GITLAB_USE_OAUTH=true");
|
|
1315
|
-
|
|
1353
|
+
exitAfterFlush(1);
|
|
1316
1354
|
}
|
|
1317
1355
|
class GitLabApiError extends Error {
|
|
1318
1356
|
constructor(message) {
|
|
@@ -1326,6 +1364,12 @@ class ToolInputError extends Error {
|
|
|
1326
1364
|
this.name = "ToolInputError";
|
|
1327
1365
|
}
|
|
1328
1366
|
}
|
|
1367
|
+
class ProjectAccessDeniedError extends Error {
|
|
1368
|
+
constructor(message) {
|
|
1369
|
+
super(message);
|
|
1370
|
+
this.name = "ProjectAccessDeniedError";
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1329
1373
|
const MERGE_REQUEST_IDENTIFIER_REQUIRED = "Either merge_request_iid or source_branch must be provided";
|
|
1330
1374
|
/**
|
|
1331
1375
|
* Utility function for handling GitLab API errors
|
|
@@ -1389,7 +1433,8 @@ async function authenticateWithJobTokenFallbackAsync(response) {
|
|
|
1389
1433
|
/**
|
|
1390
1434
|
* @param {string} projectId - The project ID parameter passed to the function
|
|
1391
1435
|
* @returns {string} The project ID to use for the API call
|
|
1392
|
-
* @throws {
|
|
1436
|
+
* @throws {ProjectAccessDeniedError} If the requested project is outside the effective allowlist
|
|
1437
|
+
* @throws {Error} If no project can be selected, or multiple allowed projects require an explicit ID
|
|
1393
1438
|
*/
|
|
1394
1439
|
function getEffectiveProjectId(projectId) {
|
|
1395
1440
|
const allowedProjectIds = getEffectiveAllowedProjectIds();
|
|
@@ -1400,7 +1445,7 @@ function getEffectiveProjectId(projectId) {
|
|
|
1400
1445
|
}
|
|
1401
1446
|
// If a project ID is provided, check if it's in the whitelist
|
|
1402
1447
|
if (projectId && !allowedProjectIds.includes(projectId)) {
|
|
1403
|
-
throw new
|
|
1448
|
+
throw new ProjectAccessDeniedError(`Access denied: Project ${projectId} is not in the allowed project list: ${allowedProjectIds.join(", ")}`);
|
|
1404
1449
|
}
|
|
1405
1450
|
// If no project ID provided but we have multiple allowed projects, require an explicit choice
|
|
1406
1451
|
if (!projectId && allowedProjectIds.length > 1) {
|
|
@@ -4643,6 +4688,28 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
|
4643
4688
|
};
|
|
4644
4689
|
}
|
|
4645
4690
|
}
|
|
4691
|
+
function healthCheckTransportErrorResult(error) {
|
|
4692
|
+
const failure = classifyHealthCheckTransportError(error);
|
|
4693
|
+
return {
|
|
4694
|
+
content: [
|
|
4695
|
+
{
|
|
4696
|
+
type: "text",
|
|
4697
|
+
text: JSON.stringify({
|
|
4698
|
+
status: "error",
|
|
4699
|
+
authenticated: false,
|
|
4700
|
+
gitlab_url: redactGitLabUrlCredentials(getEffectiveApiUrl()),
|
|
4701
|
+
mcp_server_version: SERVER_VERSION,
|
|
4702
|
+
error: {
|
|
4703
|
+
kind: failure.kind,
|
|
4704
|
+
code: failure.code,
|
|
4705
|
+
message: failure.message,
|
|
4706
|
+
...(failure.hint !== undefined ? { hint: failure.hint } : {}),
|
|
4707
|
+
},
|
|
4708
|
+
}),
|
|
4709
|
+
},
|
|
4710
|
+
],
|
|
4711
|
+
};
|
|
4712
|
+
}
|
|
4646
4713
|
async function fetchGitLabVersionMetadata() {
|
|
4647
4714
|
try {
|
|
4648
4715
|
const response = await fetch(`${getEffectiveApiUrl()}/version`, {
|
|
@@ -5442,6 +5509,132 @@ async function getEnvironment(projectId, environmentId) {
|
|
|
5442
5509
|
const data = await response.json();
|
|
5443
5510
|
return GitLabEnvironmentSchema.parse(data);
|
|
5444
5511
|
}
|
|
5512
|
+
async function listProjectRunners(projectId, options) {
|
|
5513
|
+
const decodedProjectId = decodeURIComponent(projectId);
|
|
5514
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(decodedProjectId))}/runners`);
|
|
5515
|
+
appendFilterParams(url.searchParams, options);
|
|
5516
|
+
const response = await fetch(url.toString(), {
|
|
5517
|
+
...getFetchConfig(),
|
|
5518
|
+
});
|
|
5519
|
+
await handleGitLabError(response);
|
|
5520
|
+
return redactSensitiveGitLabFields(await response.json());
|
|
5521
|
+
}
|
|
5522
|
+
const PROJECT_RUNNER_LOOKUP_PAGE_SIZE = 100;
|
|
5523
|
+
const MAX_PROJECT_RUNNER_LOOKUP_PAGES = 100;
|
|
5524
|
+
function isJsonObject(value) {
|
|
5525
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5526
|
+
}
|
|
5527
|
+
function runnerEntryId(entry) {
|
|
5528
|
+
if (!isJsonObject(entry)) {
|
|
5529
|
+
return undefined;
|
|
5530
|
+
}
|
|
5531
|
+
const id = entry.id;
|
|
5532
|
+
if (typeof id === "number" && Number.isFinite(id)) {
|
|
5533
|
+
return String(id);
|
|
5534
|
+
}
|
|
5535
|
+
if (typeof id === "string" && id !== "") {
|
|
5536
|
+
return id;
|
|
5537
|
+
}
|
|
5538
|
+
return undefined;
|
|
5539
|
+
}
|
|
5540
|
+
function projectIsInAllowlist(project, allowedProjectIds) {
|
|
5541
|
+
if (!isJsonObject(project)) {
|
|
5542
|
+
return false;
|
|
5543
|
+
}
|
|
5544
|
+
const id = project.id;
|
|
5545
|
+
const pathWithNamespace = project.path_with_namespace;
|
|
5546
|
+
const idText = typeof id === "number" && Number.isFinite(id)
|
|
5547
|
+
? String(id)
|
|
5548
|
+
: typeof id === "string"
|
|
5549
|
+
? id
|
|
5550
|
+
: undefined;
|
|
5551
|
+
return ((idText !== undefined && allowedProjectIds.includes(idText)) ||
|
|
5552
|
+
(typeof pathWithNamespace === "string" && allowedProjectIds.includes(pathWithNamespace)));
|
|
5553
|
+
}
|
|
5554
|
+
/**
|
|
5555
|
+
* GET /runners/:id is not project-scoped. Instance runner details are visible to
|
|
5556
|
+
* any authenticated user and can list projects outside GITLAB_ALLOWED_PROJECT_IDS.
|
|
5557
|
+
* Keep only projects on the effective allowlist, and drop groups because a group
|
|
5558
|
+
* path cannot be checked against a project allowlist.
|
|
5559
|
+
*/
|
|
5560
|
+
function restrictRunnerDetailsToAllowlist(runner, allowedProjectIds) {
|
|
5561
|
+
if (!isJsonObject(runner)) {
|
|
5562
|
+
return runner;
|
|
5563
|
+
}
|
|
5564
|
+
const restricted = {};
|
|
5565
|
+
for (const [key, value] of Object.entries(runner)) {
|
|
5566
|
+
if (key === "groups") {
|
|
5567
|
+
continue;
|
|
5568
|
+
}
|
|
5569
|
+
if (key === "projects" && Array.isArray(value)) {
|
|
5570
|
+
restricted.projects = value.filter(project => projectIsInAllowlist(project, allowedProjectIds));
|
|
5571
|
+
continue;
|
|
5572
|
+
}
|
|
5573
|
+
restricted[key] = value;
|
|
5574
|
+
}
|
|
5575
|
+
return restricted;
|
|
5576
|
+
}
|
|
5577
|
+
/**
|
|
5578
|
+
* Confirm the runner is among the runners available to an allowed project.
|
|
5579
|
+
* GitLab has no single-runner project read; membership is the paginated
|
|
5580
|
+
* GET /projects/:id/runners list, which includes ancestor group runners and
|
|
5581
|
+
* allowed instance runners. Fail closed when the list cannot be fully scanned.
|
|
5582
|
+
*/
|
|
5583
|
+
async function assertRunnerAvailableToProject(projectId, runnerId) {
|
|
5584
|
+
const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(projectId));
|
|
5585
|
+
let page = 1;
|
|
5586
|
+
while (page <= MAX_PROJECT_RUNNER_LOOKUP_PAGES) {
|
|
5587
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/runners`);
|
|
5588
|
+
url.searchParams.set("per_page", String(PROJECT_RUNNER_LOOKUP_PAGE_SIZE));
|
|
5589
|
+
url.searchParams.set("page", String(page));
|
|
5590
|
+
const response = await fetch(url.toString(), {
|
|
5591
|
+
...getFetchConfig(),
|
|
5592
|
+
});
|
|
5593
|
+
await handleGitLabError(response);
|
|
5594
|
+
const data = await response.json();
|
|
5595
|
+
if (!Array.isArray(data)) {
|
|
5596
|
+
throw new GitLabApiError(`Could not verify runner ${runnerId} against project ${effectiveProjectId}`);
|
|
5597
|
+
}
|
|
5598
|
+
if (data.some(entry => runnerEntryId(entry) === runnerId)) {
|
|
5599
|
+
return;
|
|
5600
|
+
}
|
|
5601
|
+
const nextPageHeader = response.headers.get("x-next-page");
|
|
5602
|
+
if (!nextPageHeader) {
|
|
5603
|
+
throw new GitLabApiError("Runner not found");
|
|
5604
|
+
}
|
|
5605
|
+
const nextPage = Number.parseInt(nextPageHeader, 10);
|
|
5606
|
+
if (!Number.isFinite(nextPage) || nextPage <= page) {
|
|
5607
|
+
throw new ProjectAccessDeniedError(`Access denied: Runner ${runnerId} could not be verified against project ${effectiveProjectId}`);
|
|
5608
|
+
}
|
|
5609
|
+
page = nextPage;
|
|
5610
|
+
}
|
|
5611
|
+
throw new ProjectAccessDeniedError(`Access denied: Runner ${runnerId} could not be verified against project ${effectiveProjectId}`);
|
|
5612
|
+
}
|
|
5613
|
+
async function getRunner(runnerId, projectId) {
|
|
5614
|
+
const allowedProjectIds = getEffectiveAllowedProjectIds();
|
|
5615
|
+
const decodedProjectId = projectId === undefined || projectId.trim() === "" ? undefined : decodeURIComponent(projectId);
|
|
5616
|
+
// GITLAB_PROJECT_ID is the default project, same as getEffectiveProjectId.
|
|
5617
|
+
// It is not an allowlist, so runner details stay unfiltered unless
|
|
5618
|
+
// GITLAB_ALLOWED_PROJECT_IDS is set.
|
|
5619
|
+
if (allowedProjectIds.length > 0 ||
|
|
5620
|
+
decodedProjectId !== undefined ||
|
|
5621
|
+
Boolean(GITLAB_PROJECT_ID)) {
|
|
5622
|
+
await assertRunnerAvailableToProject(getEffectiveProjectId(decodedProjectId ?? ""), runnerId);
|
|
5623
|
+
}
|
|
5624
|
+
const url = new URL(`${getEffectiveApiUrl()}/runners/${encodeGitLabPathSegment(runnerId)}`);
|
|
5625
|
+
const response = await fetch(url.toString(), {
|
|
5626
|
+
...getFetchConfig(),
|
|
5627
|
+
});
|
|
5628
|
+
if (response.status === 404) {
|
|
5629
|
+
throw new GitLabApiError("Runner not found");
|
|
5630
|
+
}
|
|
5631
|
+
await handleGitLabError(response);
|
|
5632
|
+
const details = redactSensitiveGitLabFields(await response.json());
|
|
5633
|
+
if (allowedProjectIds.length === 0) {
|
|
5634
|
+
return details;
|
|
5635
|
+
}
|
|
5636
|
+
return restrictRunnerDetailsToAllowlist(details, allowedProjectIds);
|
|
5637
|
+
}
|
|
5445
5638
|
async function environmentRequest(projectId, path, method, body, query) {
|
|
5446
5639
|
projectId = decodeURIComponent(projectId);
|
|
5447
5640
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/environments${path}`);
|
|
@@ -7039,7 +7232,7 @@ function assertVulnerabilityProjectAllowed(vulnerabilityId, project) {
|
|
|
7039
7232
|
const allowed = (fullPath !== undefined && allowedProjectIds.includes(fullPath)) ||
|
|
7040
7233
|
(numericId !== undefined && allowedProjectIds.includes(numericId));
|
|
7041
7234
|
if (!allowed) {
|
|
7042
|
-
throw new
|
|
7235
|
+
throw new ProjectAccessDeniedError(`Access denied: Vulnerability ${vulnerabilityId} belongs to project ${fullPath ?? numericId ?? "unknown"}, which is not in the allowed project list: ${allowedProjectIds.join(", ")}`);
|
|
7043
7236
|
}
|
|
7044
7237
|
}
|
|
7045
7238
|
/**
|
|
@@ -7863,7 +8056,7 @@ async function handleToolCall(params) {
|
|
|
7863
8056
|
const controller = new AbortController();
|
|
7864
8057
|
const timeoutMs = 45000;
|
|
7865
8058
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
7866
|
-
logger.info({ endpoint: graphqlUrl }, "execute_graphql request");
|
|
8059
|
+
logger.info({ endpoint: redactUrlSecretsInText(graphqlUrl) }, "execute_graphql request");
|
|
7867
8060
|
try {
|
|
7868
8061
|
const response = await fetch(graphqlUrl, {
|
|
7869
8062
|
...getFetchConfig(),
|
|
@@ -9286,6 +9479,20 @@ async function handleToolCall(params) {
|
|
|
9286
9479
|
content: [{ type: "text", text: JSON.stringify(environment) }],
|
|
9287
9480
|
};
|
|
9288
9481
|
}
|
|
9482
|
+
case "list_project_runners": {
|
|
9483
|
+
const { project_id, ...options } = ListProjectRunnersSchema.parse(params.arguments);
|
|
9484
|
+
const runners = await listProjectRunners(project_id, options);
|
|
9485
|
+
return {
|
|
9486
|
+
content: [{ type: "text", text: JSON.stringify(runners) }],
|
|
9487
|
+
};
|
|
9488
|
+
}
|
|
9489
|
+
case "get_runner": {
|
|
9490
|
+
const { runner_id, project_id } = GetRunnerSchema.parse(params.arguments);
|
|
9491
|
+
const runner = await getRunner(runner_id, project_id);
|
|
9492
|
+
return {
|
|
9493
|
+
content: [{ type: "text", text: JSON.stringify(runner) }],
|
|
9494
|
+
};
|
|
9495
|
+
}
|
|
9289
9496
|
case "update_environment": {
|
|
9290
9497
|
const { project_id, environment_id, ...body } = UpdateEnvironmentSchema.parse(params.arguments);
|
|
9291
9498
|
return {
|
|
@@ -10518,6 +10725,77 @@ async function handleToolCall(params) {
|
|
|
10518
10725
|
content: [{ type: "text", text: JSON.stringify(signature) }],
|
|
10519
10726
|
};
|
|
10520
10727
|
}
|
|
10728
|
+
case "list_protected_tags": {
|
|
10729
|
+
const args = ListProtectedTagsSchema.parse(params.arguments);
|
|
10730
|
+
const projectId = decodeURIComponent(args.project_id);
|
|
10731
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
10732
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_tags`);
|
|
10733
|
+
if (args.page)
|
|
10734
|
+
url.searchParams.append("page", String(args.page));
|
|
10735
|
+
if (args.per_page)
|
|
10736
|
+
url.searchParams.append("per_page", String(args.per_page));
|
|
10737
|
+
const response = await fetch(url.toString(), {
|
|
10738
|
+
...getFetchConfig(),
|
|
10739
|
+
});
|
|
10740
|
+
await handleGitLabError(response);
|
|
10741
|
+
const data = z.array(GitLabProtectedTagSchema).parse(await response.json());
|
|
10742
|
+
return {
|
|
10743
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
10744
|
+
};
|
|
10745
|
+
}
|
|
10746
|
+
case "get_protected_tag": {
|
|
10747
|
+
const args = GetProtectedTagSchema.parse(params.arguments);
|
|
10748
|
+
const projectId = decodeURIComponent(args.project_id);
|
|
10749
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
10750
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_tags/${encodeGitLabPathSegment(args.tag_name)}`);
|
|
10751
|
+
const response = await fetch(url.toString(), {
|
|
10752
|
+
...getFetchConfig(),
|
|
10753
|
+
});
|
|
10754
|
+
await handleGitLabError(response);
|
|
10755
|
+
const data = GitLabProtectedTagSchema.parse(await response.json());
|
|
10756
|
+
return {
|
|
10757
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
10758
|
+
};
|
|
10759
|
+
}
|
|
10760
|
+
case "protect_tag": {
|
|
10761
|
+
const args = ProtectTagSchema.parse(params.arguments);
|
|
10762
|
+
const projectId = decodeURIComponent(args.project_id);
|
|
10763
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
10764
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_tags`);
|
|
10765
|
+
const body = { name: args.tag_name };
|
|
10766
|
+
if (args.create_access_level !== undefined) {
|
|
10767
|
+
body.create_access_level = args.create_access_level;
|
|
10768
|
+
}
|
|
10769
|
+
const response = await fetch(url.toString(), {
|
|
10770
|
+
...getFetchConfig(),
|
|
10771
|
+
method: "POST",
|
|
10772
|
+
body: JSON.stringify(body),
|
|
10773
|
+
});
|
|
10774
|
+
await handleGitLabError(response);
|
|
10775
|
+
const data = GitLabProtectedTagSchema.parse(await response.json());
|
|
10776
|
+
return {
|
|
10777
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
10778
|
+
};
|
|
10779
|
+
}
|
|
10780
|
+
case "unprotect_tag": {
|
|
10781
|
+
const args = UnprotectTagSchema.parse(params.arguments);
|
|
10782
|
+
const projectId = decodeURIComponent(args.project_id);
|
|
10783
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
10784
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_tags/${encodeGitLabPathSegment(args.tag_name)}`);
|
|
10785
|
+
const response = await fetch(url.toString(), {
|
|
10786
|
+
...getFetchConfig(),
|
|
10787
|
+
method: "DELETE",
|
|
10788
|
+
});
|
|
10789
|
+
await handleGitLabError(response);
|
|
10790
|
+
return {
|
|
10791
|
+
content: [
|
|
10792
|
+
{
|
|
10793
|
+
type: "text",
|
|
10794
|
+
text: JSON.stringify({ status: "unprotected", tag: args.tag_name }),
|
|
10795
|
+
},
|
|
10796
|
+
],
|
|
10797
|
+
};
|
|
10798
|
+
}
|
|
10521
10799
|
case "list_snippets": {
|
|
10522
10800
|
const args = ListSnippetsSchema.parse(params.arguments);
|
|
10523
10801
|
const { project_id, ...options } = args;
|
|
@@ -10650,8 +10928,21 @@ async function handleToolCall(params) {
|
|
|
10650
10928
|
case "health_check": {
|
|
10651
10929
|
HealthCheckSchema.parse(params.arguments ?? {});
|
|
10652
10930
|
const url = new URL(`${getEffectiveApiUrl()}/user`);
|
|
10653
|
-
const
|
|
10654
|
-
|
|
10931
|
+
const fetchConfig = getFetchConfig();
|
|
10932
|
+
let response;
|
|
10933
|
+
try {
|
|
10934
|
+
response = await fetch(url.toString(), fetchConfig);
|
|
10935
|
+
}
|
|
10936
|
+
catch (error) {
|
|
10937
|
+
return healthCheckTransportErrorResult(error);
|
|
10938
|
+
}
|
|
10939
|
+
let jobAuthenticated;
|
|
10940
|
+
try {
|
|
10941
|
+
jobAuthenticated = await authenticateWithJobTokenFallbackAsync(response);
|
|
10942
|
+
}
|
|
10943
|
+
catch (error) {
|
|
10944
|
+
return healthCheckTransportErrorResult(error);
|
|
10945
|
+
}
|
|
10655
10946
|
// insufficient_scope and insufficient_granular_scope mean the token was accepted
|
|
10656
10947
|
// but User API scope is missing.
|
|
10657
10948
|
// Rate-limit and authentication-ban 403s stay unauthenticated.
|
|
@@ -10665,7 +10956,7 @@ async function handleToolCall(params) {
|
|
|
10665
10956
|
text: JSON.stringify({
|
|
10666
10957
|
status: authenticated ? "ok" : "error",
|
|
10667
10958
|
authenticated,
|
|
10668
|
-
gitlab_url: getEffectiveApiUrl(),
|
|
10959
|
+
gitlab_url: redactGitLabUrlCredentials(getEffectiveApiUrl()),
|
|
10669
10960
|
mcp_server_version: SERVER_VERSION,
|
|
10670
10961
|
...(insufficientUserScope ? { user_api_warning: USER_API_HEALTH_WARNING } : {}),
|
|
10671
10962
|
...(versionMetadata ?? {}),
|
|
@@ -11198,7 +11489,7 @@ async function startStreamableHTTPServer() {
|
|
|
11198
11489
|
}
|
|
11199
11490
|
catch (error) {
|
|
11200
11491
|
const reason = error instanceof Error ? error.message : "Invalid X-GitLab-API-URL";
|
|
11201
|
-
logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. ${reason}`);
|
|
11492
|
+
logger.warn(`Invalid X-GitLab-API-URL provided: ${redactUrlSecretsInText(dynamicApiUrl)}. ${reason}`);
|
|
11202
11493
|
return null; // Reject if URL is malformed or not allowed
|
|
11203
11494
|
}
|
|
11204
11495
|
}
|
|
@@ -12250,7 +12541,7 @@ function initOAuthClient() {
|
|
|
12250
12541
|
return;
|
|
12251
12542
|
}
|
|
12252
12543
|
if (isCleartextRemoteUrl(GITLAB_API_URL)) {
|
|
12253
|
-
throw new Error(`Refusing to send OAuth tokens over cleartext HTTP to ${GITLAB_API_URL}. ` +
|
|
12544
|
+
throw new Error(`Refusing to send OAuth tokens over cleartext HTTP to ${redactUrlSecretsInText(GITLAB_API_URL)}. ` +
|
|
12254
12545
|
"Use an https:// GITLAB_API_URL (http:// is only allowed for localhost).");
|
|
12255
12546
|
}
|
|
12256
12547
|
oauthClient = createGitLabOAuthClient(GITLAB_API_URL.replace(/\/api\/v4$/, ""));
|
|
@@ -12261,6 +12552,9 @@ function initOAuthClient() {
|
|
|
12261
12552
|
*/
|
|
12262
12553
|
async function runServer() {
|
|
12263
12554
|
try {
|
|
12555
|
+
// Print before validateConfiguration(), which process.exit()s on failure.
|
|
12556
|
+
// logger.warn goes to stderr; stdio transport owns stdout for JSON-RPC.
|
|
12557
|
+
emitDeprecatedEnvWarnings();
|
|
12264
12558
|
// Validate configuration before starting server
|
|
12265
12559
|
validateConfiguration();
|
|
12266
12560
|
// Initialize OAuth token if OAuth is enabled
|
|
@@ -12272,7 +12566,7 @@ async function runServer() {
|
|
|
12272
12566
|
}
|
|
12273
12567
|
catch (error) {
|
|
12274
12568
|
logger.error({ err: error }, "OAuth authentication failed");
|
|
12275
|
-
|
|
12569
|
+
exitAfterFlush(1);
|
|
12276
12570
|
}
|
|
12277
12571
|
}
|
|
12278
12572
|
const transportMode = determineTransportMode();
|
|
@@ -12287,35 +12581,26 @@ async function runServer() {
|
|
|
12287
12581
|
}
|
|
12288
12582
|
});
|
|
12289
12583
|
}
|
|
12290
|
-
logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.join(", ")}`);
|
|
12291
|
-
logger.info(`Default GitLab API URL: ${GITLAB_API_URL}`);
|
|
12292
|
-
for (const warning of getDeprecatedEnvWarnings({
|
|
12293
|
-
readOnlyMode: GITLAB_READ_ONLY_MODE,
|
|
12294
|
-
permissionModeRaw: GITLAB_PERMISSION_MODE_RAW,
|
|
12295
|
-
allowedGroupsRaw: GITLAB_ALLOWED_GROUPS_RAW,
|
|
12296
|
-
oauthAllowedGroupsRaw: GITLAB_OAUTH_ALLOWED_GROUPS_RAW,
|
|
12297
|
-
useWikiRaw: USE_GITLAB_WIKI_RAW,
|
|
12298
|
-
useMilestoneRaw: USE_MILESTONE_RAW,
|
|
12299
|
-
usePipelineRaw: USE_PIPELINE_RAW,
|
|
12300
|
-
})) {
|
|
12301
|
-
logger.warn(warning);
|
|
12302
|
-
}
|
|
12584
|
+
logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.map(redactUrlSecretsInText).join(", ")}`);
|
|
12585
|
+
logger.info(`Default GitLab API URL: ${redactUrlSecretsInText(GITLAB_API_URL)}`);
|
|
12303
12586
|
if (GITLAB_OAUTH_ALLOWED_GROUPS) {
|
|
12304
12587
|
logger.info(`Group access control enabled for: ${GITLAB_OAUTH_ALLOWED_GROUPS.join(", ")}`);
|
|
12305
12588
|
}
|
|
12306
12589
|
}
|
|
12307
12590
|
catch (error) {
|
|
12308
12591
|
logger.error({ err: error }, "Error initializing server");
|
|
12309
|
-
|
|
12592
|
+
exitAfterFlush(1);
|
|
12310
12593
|
}
|
|
12311
12594
|
}
|
|
12312
12595
|
async function main() {
|
|
12313
12596
|
const cli = resolveCli(process.argv);
|
|
12314
12597
|
if (cli.kind === "help") {
|
|
12598
|
+
emitDeprecatedEnvWarnings();
|
|
12315
12599
|
process.stdout.write(cli.text);
|
|
12316
12600
|
process.exit(0);
|
|
12317
12601
|
}
|
|
12318
12602
|
if (cli.kind === "usage" || cli.kind === "refused") {
|
|
12603
|
+
emitDeprecatedEnvWarnings();
|
|
12319
12604
|
process.stderr.write(`${cli.message}\n`);
|
|
12320
12605
|
process.exit(2);
|
|
12321
12606
|
}
|
|
@@ -12328,10 +12613,11 @@ async function main() {
|
|
|
12328
12613
|
const message = error instanceof Error ? error.message : String(error);
|
|
12329
12614
|
process.stderr.write(`${message}\n`);
|
|
12330
12615
|
logger.error({ err: error }, "auth command failed");
|
|
12331
|
-
|
|
12616
|
+
exitAfterFlush(1);
|
|
12332
12617
|
}
|
|
12333
12618
|
}
|
|
12334
12619
|
if (cli.kind === "run") {
|
|
12620
|
+
emitDeprecatedEnvWarnings();
|
|
12335
12621
|
if (!hasCliCredentials()) {
|
|
12336
12622
|
process.stderr.write("Missing GitLab credentials. Set --token or GITLAB_PERSONAL_ACCESS_TOKEN, or run `auth` with GITLAB_USE_OAUTH=true.\n");
|
|
12337
12623
|
process.exit(2);
|
|
@@ -12354,7 +12640,7 @@ async function main() {
|
|
|
12354
12640
|
catch (error) {
|
|
12355
12641
|
process.stderr.write(formatCliError(error));
|
|
12356
12642
|
logger.error({ err: error }, "cli command failed");
|
|
12357
|
-
|
|
12643
|
+
exitAfterFlush(1);
|
|
12358
12644
|
}
|
|
12359
12645
|
}
|
|
12360
12646
|
await runServer();
|
|
@@ -12381,5 +12667,5 @@ function maskCliToolResult(toolName, result) {
|
|
|
12381
12667
|
}
|
|
12382
12668
|
main().catch(error => {
|
|
12383
12669
|
logger.fatal({ err: error }, "Fatal error in main()");
|
|
12384
|
-
|
|
12670
|
+
exitAfterFlush(1);
|
|
12385
12671
|
});
|
package/build/oauth-proxy.js
CHANGED
|
@@ -51,6 +51,7 @@ import { looksLikeStatelessClientId, mintClientId, openClientId, } from "./state
|
|
|
51
51
|
import { looksLikeStatelessState, mintPendingAuthState, openPendingAuthState, } from "./stateless/pending-auth.js";
|
|
52
52
|
import { looksLikeStatelessStoredTokensCode, mintStoredTokensCode, openStoredTokensCode, ConsumedProxyCodeCache, PROXY_CODE_CACHE_FULL, } from "./stateless/index.js";
|
|
53
53
|
import { createLogger } from "./utils/logger.js";
|
|
54
|
+
import { redactUrlSecretsInText } from "./utils/redact-url.js";
|
|
54
55
|
const logger = createLogger("gitlab-mcp-oauth-proxy");
|
|
55
56
|
// ---------------------------------------------------------------------------
|
|
56
57
|
// GitLab OAuth Server Provider
|
|
@@ -162,7 +163,7 @@ class GitLabOAuthServerProvider {
|
|
|
162
163
|
throw new Error("callbackUrl is required when callbackProxyEnabled is true");
|
|
163
164
|
}
|
|
164
165
|
if (callbackProxyEnabled) {
|
|
165
|
-
logger.info(`Callback proxy mode enabled — fixed callback URL: ${callbackUrl}`);
|
|
166
|
+
logger.info(`Callback proxy mode enabled — fixed callback URL: ${redactUrlSecretsInText(callbackUrl)}`);
|
|
166
167
|
}
|
|
167
168
|
if (stateless) {
|
|
168
169
|
logger.info(`Stateless mode enabled (client_id TTL: ${stateless.clientTtlSeconds}s, ` +
|