@zereight/mcp-gitlab 2.2.0 → 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 +24 -15
- package/README.md +86 -73
- package/README.pt-BR.md +25 -14
- package/README.zh-CN.md +23 -14
- 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 +166 -20
- package/build/index.js +387 -56
- package/build/oauth-proxy.js +2 -1
- package/build/oauth.js +5 -4
- package/build/schemas.js +97 -0
- package/build/tools/registry.js +60 -9
- package/build/tools/tool-descriptions.js +7 -3
- 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,9 +509,11 @@ function createServer() {
|
|
|
502
509
|
return logCompletion(result);
|
|
503
510
|
}
|
|
504
511
|
catch (error) {
|
|
505
|
-
const
|
|
512
|
+
const isRecoverableToolError = error instanceof GitLabApiError ||
|
|
513
|
+
error instanceof ToolInputError ||
|
|
514
|
+
error instanceof ProjectAccessDeniedError;
|
|
506
515
|
const safeError = logError(error);
|
|
507
|
-
if (
|
|
516
|
+
if (isRecoverableToolError) {
|
|
508
517
|
return {
|
|
509
518
|
content: [
|
|
510
519
|
{
|
|
@@ -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) {
|
|
@@ -1320,6 +1358,19 @@ class GitLabApiError extends Error {
|
|
|
1320
1358
|
this.name = "GitLabApiError";
|
|
1321
1359
|
}
|
|
1322
1360
|
}
|
|
1361
|
+
class ToolInputError extends Error {
|
|
1362
|
+
constructor(message) {
|
|
1363
|
+
super(message);
|
|
1364
|
+
this.name = "ToolInputError";
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
class ProjectAccessDeniedError extends Error {
|
|
1368
|
+
constructor(message) {
|
|
1369
|
+
super(message);
|
|
1370
|
+
this.name = "ProjectAccessDeniedError";
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
const MERGE_REQUEST_IDENTIFIER_REQUIRED = "Either merge_request_iid or source_branch must be provided";
|
|
1323
1374
|
/**
|
|
1324
1375
|
* Utility function for handling GitLab API errors
|
|
1325
1376
|
* API 에러 처리를 위한 유틸리티 함수 (Utility function for handling API errors)
|
|
@@ -1346,10 +1397,44 @@ async function handleGitLabError(response) {
|
|
|
1346
1397
|
}
|
|
1347
1398
|
}
|
|
1348
1399
|
}
|
|
1400
|
+
const USER_API_INSUFFICIENT_SCOPE_MESSAGE = "GitLab API error: 403 Forbidden on GET /api/v4/user (insufficient scope). The token was accepted, but read_user (classic PAT) or User API read (fine-grained User:Read) is required for whoami, my_issues, and other current-user tools.";
|
|
1401
|
+
const USER_API_HEALTH_WARNING = "GET /api/v4/user returned 403 (insufficient scope). The token was accepted and other tools may still work. Grant read_user (classic) or User:Read (fine-grained) for whoami, my_issues, and version metadata.";
|
|
1402
|
+
function isGitLabUserApiRateLimitBody(errorBody) {
|
|
1403
|
+
return errorBody.includes("User API Key Rate limit exceeded");
|
|
1404
|
+
}
|
|
1405
|
+
function isGitLabInsufficientUserScopeBody(errorBody) {
|
|
1406
|
+
// Fine-grained PATs use insufficient_granular_scope, which does not contain insufficient_scope.
|
|
1407
|
+
return (errorBody.includes("insufficient_granular_scope") || errorBody.includes("insufficient_scope"));
|
|
1408
|
+
}
|
|
1409
|
+
async function isInsufficientUserScopeForbiddenAsync(response) {
|
|
1410
|
+
if (response.status !== 403) {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
const errorBody = await response.text();
|
|
1414
|
+
return isGitLabInsufficientUserScopeBody(errorBody);
|
|
1415
|
+
}
|
|
1416
|
+
async function throwUserApiScopeErrorUnlessRateLimitedAsync(response) {
|
|
1417
|
+
const errorBody = await response.clone().text();
|
|
1418
|
+
// Rate-limit and other 403s keep the upstream body via handleGitLabError.
|
|
1419
|
+
if (isGitLabUserApiRateLimitBody(errorBody) || !isGitLabInsufficientUserScopeBody(errorBody)) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
throw new GitLabApiError(USER_API_INSUFFICIENT_SCOPE_MESSAGE);
|
|
1423
|
+
}
|
|
1424
|
+
async function authenticateWithJobTokenFallbackAsync(response) {
|
|
1425
|
+
if (response.ok ||
|
|
1426
|
+
(response.status !== 401 && response.status !== 403) ||
|
|
1427
|
+
!(GITLAB_JOB_TOKEN || usesJobTokenHeader())) {
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
const jobResponse = await fetch(`${getEffectiveApiUrl()}/job`, getFetchConfig());
|
|
1431
|
+
return jobResponse.ok;
|
|
1432
|
+
}
|
|
1349
1433
|
/**
|
|
1350
1434
|
* @param {string} projectId - The project ID parameter passed to the function
|
|
1351
1435
|
* @returns {string} The project ID to use for the API call
|
|
1352
|
-
* @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
|
|
1353
1438
|
*/
|
|
1354
1439
|
function getEffectiveProjectId(projectId) {
|
|
1355
1440
|
const allowedProjectIds = getEffectiveAllowedProjectIds();
|
|
@@ -1360,7 +1445,7 @@ function getEffectiveProjectId(projectId) {
|
|
|
1360
1445
|
}
|
|
1361
1446
|
// If a project ID is provided, check if it's in the whitelist
|
|
1362
1447
|
if (projectId && !allowedProjectIds.includes(projectId)) {
|
|
1363
|
-
throw new
|
|
1448
|
+
throw new ProjectAccessDeniedError(`Access denied: Project ${projectId} is not in the allowed project list: ${allowedProjectIds.join(", ")}`);
|
|
1364
1449
|
}
|
|
1365
1450
|
// If no project ID provided but we have multiple allowed projects, require an explicit choice
|
|
1366
1451
|
if (!projectId && allowedProjectIds.length > 1) {
|
|
@@ -3793,7 +3878,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
|
|
|
3793
3878
|
url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
|
|
3794
3879
|
}
|
|
3795
3880
|
else {
|
|
3796
|
-
throw new
|
|
3881
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
3797
3882
|
}
|
|
3798
3883
|
const response = await fetch(url.toString(), {
|
|
3799
3884
|
...getFetchConfig(),
|
|
@@ -4027,7 +4112,7 @@ async function buildMergeRequestDeploymentSummary(projectId, mergeRequest) {
|
|
|
4027
4112
|
async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view) {
|
|
4028
4113
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4029
4114
|
if (!mergeRequestIid && !branchName) {
|
|
4030
|
-
throw new
|
|
4115
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4031
4116
|
}
|
|
4032
4117
|
if (branchName && !mergeRequestIid) {
|
|
4033
4118
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -4057,7 +4142,7 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view
|
|
|
4057
4142
|
async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, page, perPage, unidiff) {
|
|
4058
4143
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4059
4144
|
if (!mergeRequestIid && !branchName) {
|
|
4060
|
-
throw new
|
|
4145
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4061
4146
|
}
|
|
4062
4147
|
if (branchName && !mergeRequestIid) {
|
|
4063
4148
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -4093,7 +4178,7 @@ async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, pag
|
|
|
4093
4178
|
async function listMergeRequestChangedFiles(projectId, mergeRequestIid, branchName, excludedFilePatterns) {
|
|
4094
4179
|
projectId = decodeURIComponent(projectId);
|
|
4095
4180
|
if (!mergeRequestIid && !branchName) {
|
|
4096
|
-
throw new
|
|
4181
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4097
4182
|
}
|
|
4098
4183
|
if (branchName && !mergeRequestIid) {
|
|
4099
4184
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -4127,7 +4212,7 @@ async function listMergeRequestChangedFiles(projectId, mergeRequestIid, branchNa
|
|
|
4127
4212
|
async function getMergeRequestFileDiff(projectId, filePaths, mergeRequestIid, branchName, unidiff) {
|
|
4128
4213
|
projectId = decodeURIComponent(projectId);
|
|
4129
4214
|
if (!mergeRequestIid && !branchName) {
|
|
4130
|
-
throw new
|
|
4215
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4131
4216
|
}
|
|
4132
4217
|
if (branchName && !mergeRequestIid) {
|
|
4133
4218
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -4212,7 +4297,7 @@ async function getBranchDiffs(projectId, from, to, straight) {
|
|
|
4212
4297
|
async function updateMergeRequest(projectId, options, mergeRequestIid, branchName) {
|
|
4213
4298
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4214
4299
|
if (!mergeRequestIid && !branchName) {
|
|
4215
|
-
throw new
|
|
4300
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4216
4301
|
}
|
|
4217
4302
|
if (branchName && !mergeRequestIid) {
|
|
4218
4303
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -4603,6 +4688,28 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
|
4603
4688
|
};
|
|
4604
4689
|
}
|
|
4605
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
|
+
}
|
|
4606
4713
|
async function fetchGitLabVersionMetadata() {
|
|
4607
4714
|
try {
|
|
4608
4715
|
const response = await fetch(`${getEffectiveApiUrl()}/version`, {
|
|
@@ -5402,6 +5509,132 @@ async function getEnvironment(projectId, environmentId) {
|
|
|
5402
5509
|
const data = await response.json();
|
|
5403
5510
|
return GitLabEnvironmentSchema.parse(data);
|
|
5404
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
|
+
}
|
|
5405
5638
|
async function environmentRequest(projectId, path, method, body, query) {
|
|
5406
5639
|
projectId = decodeURIComponent(projectId);
|
|
5407
5640
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/environments${path}`);
|
|
@@ -6593,6 +6826,9 @@ async function getCurrentUser() {
|
|
|
6593
6826
|
}
|
|
6594
6827
|
}
|
|
6595
6828
|
}
|
|
6829
|
+
if (response.status === 403) {
|
|
6830
|
+
await throwUserApiScopeErrorUnlessRateLimitedAsync(response);
|
|
6831
|
+
}
|
|
6596
6832
|
await handleGitLabError(response);
|
|
6597
6833
|
throw new Error(`GitLab API error: ${response.status} ${response.statusText}`);
|
|
6598
6834
|
}
|
|
@@ -6996,7 +7232,7 @@ function assertVulnerabilityProjectAllowed(vulnerabilityId, project) {
|
|
|
6996
7232
|
const allowed = (fullPath !== undefined && allowedProjectIds.includes(fullPath)) ||
|
|
6997
7233
|
(numericId !== undefined && allowedProjectIds.includes(numericId));
|
|
6998
7234
|
if (!allowed) {
|
|
6999
|
-
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(", ")}`);
|
|
7000
7236
|
}
|
|
7001
7237
|
}
|
|
7002
7238
|
/**
|
|
@@ -7820,7 +8056,7 @@ async function handleToolCall(params) {
|
|
|
7820
8056
|
const controller = new AbortController();
|
|
7821
8057
|
const timeoutMs = 45000;
|
|
7822
8058
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
7823
|
-
logger.info({ endpoint: graphqlUrl }, "execute_graphql request");
|
|
8059
|
+
logger.info({ endpoint: redactUrlSecretsInText(graphqlUrl) }, "execute_graphql request");
|
|
7824
8060
|
try {
|
|
7825
8061
|
const response = await fetch(graphqlUrl, {
|
|
7826
8062
|
...getFetchConfig(),
|
|
@@ -8512,6 +8748,9 @@ async function handleToolCall(params) {
|
|
|
8512
8748
|
const response = await fetch(url.toString(), {
|
|
8513
8749
|
...getFetchConfig(),
|
|
8514
8750
|
});
|
|
8751
|
+
if (response.status === 403) {
|
|
8752
|
+
await throwUserApiScopeErrorUnlessRateLimitedAsync(response);
|
|
8753
|
+
}
|
|
8515
8754
|
await handleGitLabError(response);
|
|
8516
8755
|
const data = await response.json();
|
|
8517
8756
|
const user = GitLabCurrentUserSchema.parse(data);
|
|
@@ -9240,6 +9479,20 @@ async function handleToolCall(params) {
|
|
|
9240
9479
|
content: [{ type: "text", text: JSON.stringify(environment) }],
|
|
9241
9480
|
};
|
|
9242
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
|
+
}
|
|
9243
9496
|
case "update_environment": {
|
|
9244
9497
|
const { project_id, environment_id, ...body } = UpdateEnvironmentSchema.parse(params.arguments);
|
|
9245
9498
|
return {
|
|
@@ -10472,6 +10725,77 @@ async function handleToolCall(params) {
|
|
|
10472
10725
|
content: [{ type: "text", text: JSON.stringify(signature) }],
|
|
10473
10726
|
};
|
|
10474
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
|
+
}
|
|
10475
10799
|
case "list_snippets": {
|
|
10476
10800
|
const args = ListSnippetsSchema.parse(params.arguments);
|
|
10477
10801
|
const { project_id, ...options } = args;
|
|
@@ -10604,16 +10928,27 @@ async function handleToolCall(params) {
|
|
|
10604
10928
|
case "health_check": {
|
|
10605
10929
|
HealthCheckSchema.parse(params.arguments ?? {});
|
|
10606
10930
|
const url = new URL(`${getEffectiveApiUrl()}/user`);
|
|
10607
|
-
const
|
|
10608
|
-
let
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
(GITLAB_JOB_TOKEN || usesJobTokenHeader())) {
|
|
10612
|
-
const jobUrl = new URL(`${getEffectiveApiUrl()}/job`);
|
|
10613
|
-
const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
|
|
10614
|
-
authenticated = jobResponse.ok;
|
|
10931
|
+
const fetchConfig = getFetchConfig();
|
|
10932
|
+
let response;
|
|
10933
|
+
try {
|
|
10934
|
+
response = await fetch(url.toString(), fetchConfig);
|
|
10615
10935
|
}
|
|
10616
|
-
|
|
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
|
+
}
|
|
10946
|
+
// insufficient_scope and insufficient_granular_scope mean the token was accepted
|
|
10947
|
+
// but User API scope is missing.
|
|
10948
|
+
// Rate-limit and authentication-ban 403s stay unauthenticated.
|
|
10949
|
+
const insufficientUserScope = !jobAuthenticated && (await isInsufficientUserScopeForbiddenAsync(response));
|
|
10950
|
+
const authenticated = response.ok || jobAuthenticated || insufficientUserScope;
|
|
10951
|
+
const versionMetadata = authenticated && !insufficientUserScope ? await fetchGitLabVersionMetadata() : null;
|
|
10617
10952
|
return {
|
|
10618
10953
|
content: [
|
|
10619
10954
|
{
|
|
@@ -10621,8 +10956,9 @@ async function handleToolCall(params) {
|
|
|
10621
10956
|
text: JSON.stringify({
|
|
10622
10957
|
status: authenticated ? "ok" : "error",
|
|
10623
10958
|
authenticated,
|
|
10624
|
-
gitlab_url: getEffectiveApiUrl(),
|
|
10959
|
+
gitlab_url: redactGitLabUrlCredentials(getEffectiveApiUrl()),
|
|
10625
10960
|
mcp_server_version: SERVER_VERSION,
|
|
10961
|
+
...(insufficientUserScope ? { user_api_warning: USER_API_HEALTH_WARNING } : {}),
|
|
10626
10962
|
...(versionMetadata ?? {}),
|
|
10627
10963
|
}),
|
|
10628
10964
|
},
|
|
@@ -11153,7 +11489,7 @@ async function startStreamableHTTPServer() {
|
|
|
11153
11489
|
}
|
|
11154
11490
|
catch (error) {
|
|
11155
11491
|
const reason = error instanceof Error ? error.message : "Invalid X-GitLab-API-URL";
|
|
11156
|
-
logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. ${reason}`);
|
|
11492
|
+
logger.warn(`Invalid X-GitLab-API-URL provided: ${redactUrlSecretsInText(dynamicApiUrl)}. ${reason}`);
|
|
11157
11493
|
return null; // Reject if URL is malformed or not allowed
|
|
11158
11494
|
}
|
|
11159
11495
|
}
|
|
@@ -12205,7 +12541,7 @@ function initOAuthClient() {
|
|
|
12205
12541
|
return;
|
|
12206
12542
|
}
|
|
12207
12543
|
if (isCleartextRemoteUrl(GITLAB_API_URL)) {
|
|
12208
|
-
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)}. ` +
|
|
12209
12545
|
"Use an https:// GITLAB_API_URL (http:// is only allowed for localhost).");
|
|
12210
12546
|
}
|
|
12211
12547
|
oauthClient = createGitLabOAuthClient(GITLAB_API_URL.replace(/\/api\/v4$/, ""));
|
|
@@ -12216,6 +12552,9 @@ function initOAuthClient() {
|
|
|
12216
12552
|
*/
|
|
12217
12553
|
async function runServer() {
|
|
12218
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();
|
|
12219
12558
|
// Validate configuration before starting server
|
|
12220
12559
|
validateConfiguration();
|
|
12221
12560
|
// Initialize OAuth token if OAuth is enabled
|
|
@@ -12227,7 +12566,7 @@ async function runServer() {
|
|
|
12227
12566
|
}
|
|
12228
12567
|
catch (error) {
|
|
12229
12568
|
logger.error({ err: error }, "OAuth authentication failed");
|
|
12230
|
-
|
|
12569
|
+
exitAfterFlush(1);
|
|
12231
12570
|
}
|
|
12232
12571
|
}
|
|
12233
12572
|
const transportMode = determineTransportMode();
|
|
@@ -12242,35 +12581,26 @@ async function runServer() {
|
|
|
12242
12581
|
}
|
|
12243
12582
|
});
|
|
12244
12583
|
}
|
|
12245
|
-
logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.join(", ")}`);
|
|
12246
|
-
logger.info(`Default GitLab API URL: ${GITLAB_API_URL}`);
|
|
12247
|
-
for (const warning of getDeprecatedEnvWarnings({
|
|
12248
|
-
readOnlyMode: GITLAB_READ_ONLY_MODE,
|
|
12249
|
-
permissionModeRaw: GITLAB_PERMISSION_MODE_RAW,
|
|
12250
|
-
allowedGroupsRaw: GITLAB_ALLOWED_GROUPS_RAW,
|
|
12251
|
-
oauthAllowedGroupsRaw: GITLAB_OAUTH_ALLOWED_GROUPS_RAW,
|
|
12252
|
-
useWikiRaw: USE_GITLAB_WIKI_RAW,
|
|
12253
|
-
useMilestoneRaw: USE_MILESTONE_RAW,
|
|
12254
|
-
usePipelineRaw: USE_PIPELINE_RAW,
|
|
12255
|
-
})) {
|
|
12256
|
-
logger.warn(warning);
|
|
12257
|
-
}
|
|
12584
|
+
logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.map(redactUrlSecretsInText).join(", ")}`);
|
|
12585
|
+
logger.info(`Default GitLab API URL: ${redactUrlSecretsInText(GITLAB_API_URL)}`);
|
|
12258
12586
|
if (GITLAB_OAUTH_ALLOWED_GROUPS) {
|
|
12259
12587
|
logger.info(`Group access control enabled for: ${GITLAB_OAUTH_ALLOWED_GROUPS.join(", ")}`);
|
|
12260
12588
|
}
|
|
12261
12589
|
}
|
|
12262
12590
|
catch (error) {
|
|
12263
12591
|
logger.error({ err: error }, "Error initializing server");
|
|
12264
|
-
|
|
12592
|
+
exitAfterFlush(1);
|
|
12265
12593
|
}
|
|
12266
12594
|
}
|
|
12267
12595
|
async function main() {
|
|
12268
12596
|
const cli = resolveCli(process.argv);
|
|
12269
12597
|
if (cli.kind === "help") {
|
|
12598
|
+
emitDeprecatedEnvWarnings();
|
|
12270
12599
|
process.stdout.write(cli.text);
|
|
12271
12600
|
process.exit(0);
|
|
12272
12601
|
}
|
|
12273
12602
|
if (cli.kind === "usage" || cli.kind === "refused") {
|
|
12603
|
+
emitDeprecatedEnvWarnings();
|
|
12274
12604
|
process.stderr.write(`${cli.message}\n`);
|
|
12275
12605
|
process.exit(2);
|
|
12276
12606
|
}
|
|
@@ -12283,10 +12613,11 @@ async function main() {
|
|
|
12283
12613
|
const message = error instanceof Error ? error.message : String(error);
|
|
12284
12614
|
process.stderr.write(`${message}\n`);
|
|
12285
12615
|
logger.error({ err: error }, "auth command failed");
|
|
12286
|
-
|
|
12616
|
+
exitAfterFlush(1);
|
|
12287
12617
|
}
|
|
12288
12618
|
}
|
|
12289
12619
|
if (cli.kind === "run") {
|
|
12620
|
+
emitDeprecatedEnvWarnings();
|
|
12290
12621
|
if (!hasCliCredentials()) {
|
|
12291
12622
|
process.stderr.write("Missing GitLab credentials. Set --token or GITLAB_PERSONAL_ACCESS_TOKEN, or run `auth` with GITLAB_USE_OAUTH=true.\n");
|
|
12292
12623
|
process.exit(2);
|
|
@@ -12309,7 +12640,7 @@ async function main() {
|
|
|
12309
12640
|
catch (error) {
|
|
12310
12641
|
process.stderr.write(formatCliError(error));
|
|
12311
12642
|
logger.error({ err: error }, "cli command failed");
|
|
12312
|
-
|
|
12643
|
+
exitAfterFlush(1);
|
|
12313
12644
|
}
|
|
12314
12645
|
}
|
|
12315
12646
|
await runServer();
|
|
@@ -12336,5 +12667,5 @@ function maskCliToolResult(toolName, result) {
|
|
|
12336
12667
|
}
|
|
12337
12668
|
main().catch(error => {
|
|
12338
12669
|
logger.fatal({ err: error }, "Fatal error in main()");
|
|
12339
|
-
|
|
12670
|
+
exitAfterFlush(1);
|
|
12340
12671
|
});
|