@zereight/mcp-gitlab 2.1.69 → 2.2.0
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 +10 -4
- package/README.md +238 -233
- package/README.pt-BR.md +4 -4
- package/README.zh-CN.md +6 -4
- package/build/config.js +12 -4
- package/build/deprecated-env.js +47 -0
- package/build/index.js +456 -237
- package/build/oauth-device-flow.js +80 -0
- package/build/oauth.js +135 -18
- package/build/schemas.js +1 -1
- package/build/server/remote-http-stack.js +39 -0
- package/build/tools/discover-activation.js +38 -0
- package/build/tools/exposed-tools.js +35 -0
- package/build/tools/listed-tools.js +76 -0
- package/build/tools/registry.js +226 -57
- package/build/tools/tool-profile.js +66 -0
- package/build/utils/compact-tool-allowlist.js +14 -0
- package/build/utils/logger.js +34 -12
- package/build/utils/url.js +15 -0
- package/package.json +3 -3
- package/build/scripts/check-skill-sync.js +0 -143
- package/build/scripts/generate-server-card.js +0 -39
- package/build/scripts/generate-tool-coverage.js +0 -58
- package/build/scripts/generate-tool-docs.js +0 -430
- package/build/scripts/tool-coverage/coverage.js +0 -341
- package/build/scripts/tool-coverage/coverage.test.js +0 -142
- package/build/test/callback-proxy-tests.js +0 -321
- package/build/test/cli/args.test.js +0 -175
- package/build/test/cli/boolean-flags.test.js +0 -106
- package/build/test/cli/compact-mcp-result.test.js +0 -93
- package/build/test/cli/exposure.test.js +0 -104
- package/build/test/cli/load-config-snapshot.js +0 -5
- package/build/test/cli/output.test.js +0 -74
- package/build/test/cli/replay-command.test.js +0 -37
- package/build/test/cli/router.test.js +0 -183
- package/build/test/client-pool-test.js +0 -109
- package/build/test/clients/client.js +0 -34
- package/build/test/clients/custom-header-client.js +0 -122
- package/build/test/clients/sse-client.js +0 -95
- package/build/test/clients/stdio-client.js +0 -113
- package/build/test/clients/streamable-http-client.js +0 -95
- package/build/test/config-allowed-groups.test.js +0 -97
- package/build/test/create-commit-actions.test.js +0 -102
- package/build/test/draft-note-schema.test.js +0 -44
- package/build/test/dynamic-api-url-allowlist.test.js +0 -121
- package/build/test/dynamic-api-url-test.js +0 -304
- package/build/test/dynamic-routing-tests.js +0 -495
- package/build/test/get-merge-request-discussion-schema.test.js +0 -30
- package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
- package/build/test/gitlab-diff-schema.test.js +0 -36
- package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
- package/build/test/gitlab-tag-schema.test.js +0 -51
- package/build/test/group-milestone-schema.test.js +0 -45
- package/build/test/mcp-oauth-tests.js +0 -905
- package/build/test/mcp-server-name.test.js +0 -87
- package/build/test/merge-merge-request-schema.test.js +0 -75
- package/build/test/multi-server-test.js +0 -190
- package/build/test/no-proxy-integration-test.js +0 -183
- package/build/test/no-proxy-test.js +0 -149
- package/build/test/nullable-gitlab-response-fields.test.js +0 -37
- package/build/test/nullish-tool-arguments-schema.test.js +0 -118
- package/build/test/oauth-device-flow-tests.js +0 -450
- package/build/test/oauth-startup.test.js +0 -204
- package/build/test/oauth-tests.js +0 -428
- package/build/test/path-segment-encoding.test.js +0 -98
- package/build/test/remote-auth-simple-test.js +0 -277
- package/build/test/remote-auth-tests.js +0 -314
- package/build/test/response-masking.test.js +0 -445
- package/build/test/schema-tests.js +0 -1679
- package/build/test/server/metrics.test.js +0 -47
- package/build/test/server/request-helpers.test.js +0 -48
- package/build/test/server/version.test.js +0 -13
- package/build/test/sse-auth-guard.test.js +0 -96
- package/build/test/sse-session-limits.test.js +0 -273
- package/build/test/stateless/callback-proxy.test.js +0 -510
- package/build/test/stateless/client-id.test.js +0 -177
- package/build/test/stateless/codec.test.js +0 -328
- package/build/test/stateless/config-ttl.test.js +0 -149
- package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
- package/build/test/stateless/session-id-integration.test.js +0 -675
- package/build/test/stateless/session-id.test.js +0 -157
- package/build/test/streamable-http-concurrent-session.test.js +0 -171
- package/build/test/streamable-http-dns-rebinding.test.js +0 -185
- package/build/test/streamable-http-sse-stream.test.js +0 -350
- package/build/test/streamable-http-static-token-auth.test.js +0 -157
- package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
- package/build/test/test-all-transport-server.js +0 -255
- package/build/test/test-auth-retry.js +0 -195
- package/build/test/test-blank-filters.js +0 -156
- package/build/test/test-ci-catalog.js +0 -177
- package/build/test/test-ci-lint.js +0 -191
- package/build/test/test-ci-variables.js +0 -309
- package/build/test/test-create-repository.js +0 -120
- package/build/test/test-dependency-proxy.js +0 -195
- package/build/test/test-deployment-tools.js +0 -463
- package/build/test/test-download-attachment.js +0 -190
- package/build/test/test-dynamic-project-scope.js +0 -524
- package/build/test/test-empty-toolsets.js +0 -535
- package/build/test/test-get-file-blame.js +0 -145
- package/build/test/test-get-file-contents.js +0 -128
- package/build/test/test-geteffectiveprojectid.js +0 -812
- package/build/test/test-health-check.js +0 -115
- package/build/test/test-issue-description-patch.js +0 -256
- package/build/test/test-job-artifacts.js +0 -275
- package/build/test/test-json-schema.js +0 -166
- package/build/test/test-list-group-members.js +0 -150
- package/build/test/test-list-group-merge-requests.js +0 -165
- package/build/test/test-list-issues.js +0 -170
- package/build/test/test-list-merge-requests.js +0 -201
- package/build/test/test-list-milestones.js +0 -145
- package/build/test/test-list-project-members.js +0 -132
- package/build/test/test-merge-request-approval-state-tools.js +0 -213
- package/build/test/test-merge-request-approvals.js +0 -187
- package/build/test/test-merge-request-pipelines.js +0 -106
- package/build/test/test-mr-diffs-filter.js +0 -132
- package/build/test/test-mr-file-diffs.js +0 -251
- package/build/test/test-oauth-proxy-rate-limit.js +0 -133
- package/build/test/test-orbit.js +0 -155
- package/build/test/test-permission-mode.js +0 -424
- package/build/test/test-pipeline-schedules.js +0 -482
- package/build/test/test-protected-branches.js +0 -174
- package/build/test/test-remote-downloads.js +0 -510
- package/build/test/test-search-code.js +0 -275
- package/build/test/test-snippets.js +0 -735
- package/build/test/test-tags.js +0 -220
- package/build/test/test-todos.js +0 -195
- package/build/test/test-token-optimizations.js +0 -691
- package/build/test/test-toolset-filtering.js +0 -499
- package/build/test/test-update-issue-slim.js +0 -141
- package/build/test/test-update-project.js +0 -112
- package/build/test/test-upload-markdown.js +0 -206
- package/build/test/test-vulnerabilities.js +0 -399
- package/build/test/test-webhooks.js +0 -218
- package/build/test/tool-description-quality.test.js +0 -50
- package/build/test/utils/bulk-publish-options.test.js +0 -32
- package/build/test/utils/download-token.test.js +0 -35
- package/build/test/utils/forwarded-public-base-url.test.js +0 -46
- package/build/test/utils/gitlab-instance-version.test.js +0 -32
- package/build/test/utils/gitlab-version-gate.test.js +0 -74
- package/build/test/utils/graphql-query.test.js +0 -168
- package/build/test/utils/helpers-path.test.js +0 -104
- package/build/test/utils/jmespath-tool-result.test.js +0 -91
- package/build/test/utils/masking.test.js +0 -384
- package/build/test/utils/merge-request-position.test.js +0 -57
- package/build/test/utils/mock-gitlab-server.js +0 -614
- package/build/test/utils/proxy-client-ip.test.js +0 -28
- package/build/test/utils/redact-sensitive.test.js +0 -32
- package/build/test/utils/safe-redirect-fetch.test.js +0 -463
- package/build/test/utils/server-launcher.js +0 -231
- package/build/test/utils/tool-args.test.js +0 -210
- package/build/test/utils/url.test.js +0 -128
- package/build/test/utils/version-check.test.js +0 -239
- package/build/test/utils/wiki-title.test.js +0 -21
- package/build/test-note.js +0 -54
- package/build/test-resolve-issue-note.js +0 -127
package/build/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_MASKING_ENABLED, GITLAB_MASKING_CONFIG, GITLAB_MASKING_POLICY_FILE, GITLAB_MASKING_WORKSPACE_DIR, GITLAB_PERMISSION_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_RESULT_CHARS, } from "./config.js";
|
|
2
|
+
import { getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_MASKING_ENABLED, GITLAB_MASKING_CONFIG, GITLAB_MASKING_POLICY_FILE, GITLAB_MASKING_WORKSPACE_DIR, GITLAB_PERMISSION_MODE, GITLAB_PERMISSION_MODE_RAW, 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_GITLAB_WIKI_RAW, USE_MILESTONE, USE_MILESTONE_RAW, USE_OAUTH, USE_PIPELINE, USE_PIPELINE_RAW, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_RESULT_CHARS, GITLAB_MCP_COMPACT_TOOL_NAMES, GITLAB_TOOL_PROFILE, } from "./config.js";
|
|
3
|
+
import { getDeprecatedEnvWarnings } from "./deprecated-env.js";
|
|
3
4
|
/** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
|
|
4
5
|
const IS_REMOTE = SSE || STREAMABLE_HTTP;
|
|
5
6
|
const STREAMABLE_HTTP_AUTH_TOKEN = getConfig("streamable-http-auth-token", "STREAMABLE_HTTP_AUTH_TOKEN");
|
|
@@ -38,7 +39,8 @@ function buildDownloadUrl(type, params) {
|
|
|
38
39
|
header = "JOB-TOKEN";
|
|
39
40
|
headerValue = String(staticToken);
|
|
40
41
|
}
|
|
41
|
-
else if (IS_OLD) {
|
|
42
|
+
else if (IS_OLD && !OAUTH_ACCESS_TOKEN) {
|
|
43
|
+
// OAuth tokens always use Bearer (see buildAuthHeaders)
|
|
42
44
|
header = "Private-Token";
|
|
43
45
|
headerValue = String(staticToken);
|
|
44
46
|
}
|
|
@@ -59,17 +61,14 @@ function isConstantTimeSecretMatch(provided, expected) {
|
|
|
59
61
|
}
|
|
60
62
|
import { loadKeyMaterialFromEnv, looksLikeStatelessSessionId, mintSessionId, openSessionId, } from "./stateless/index.js";
|
|
61
63
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
62
|
-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
63
64
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
64
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
65
65
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
66
66
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
67
|
-
import express from "express";
|
|
68
67
|
import fetchCookie from "fetch-cookie";
|
|
69
68
|
import fs from "node:fs";
|
|
70
69
|
import { pipeline as streamPipeline } from "node:stream/promises";
|
|
71
70
|
import os from "node:os";
|
|
72
|
-
import { fetch as undiciFetch, File, FormData } from "undici";
|
|
71
|
+
import { fetch as undiciFetch, File, FormData, } from "undici";
|
|
73
72
|
import path from "node:path";
|
|
74
73
|
import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
75
74
|
import { URL } from "node:url";
|
|
@@ -82,18 +81,17 @@ import { formatCliError, formatToolOutput } from "./cli/output.js";
|
|
|
82
81
|
import { compactMcpToolResult, readOwnRecord } from "./cli/compact-mcp-result.js";
|
|
83
82
|
import { compileDeniedToolsRegex } from "./tools/denied-regex.js";
|
|
84
83
|
import { createGitLabOAuthProvider } from "./oauth-proxy.js";
|
|
85
|
-
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
86
|
-
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
|
87
84
|
import { normalizeProxyClientIpForRateLimit } from "./utils/proxy-client-ip.js";
|
|
88
85
|
import { getForwardedPublicBaseUrl, getForwardedRequestHost, } from "./utils/forwarded-public-base-url.js";
|
|
89
86
|
import { registerDownloadProxy } from "./downloads/proxy.js";
|
|
90
87
|
import { createDownloadToken } from "./utils/download-token.js";
|
|
88
|
+
import { loadRemoteHttpStack } from "./server/remote-http-stack.js";
|
|
91
89
|
import { determineTransportMode, TransportMode } from "./server/transport-mode.js";
|
|
92
90
|
import { formatPrometheusMetrics } from "./server/metrics.js";
|
|
93
91
|
import { SERVER_VERSION } from "./server/version.js";
|
|
94
92
|
import { isInitializationRequestBody, isUnauthenticatedDiscoveryRequestBody, readAcceptHeader, readMcpSessionIdHeader, redactSessionIdForLog, } from "./server/request-helpers.js";
|
|
95
93
|
export { readMcpSessionIdHeader } from "./server/request-helpers.js";
|
|
96
|
-
import { encodeGitLabPath, encodeGitLabPathSegment, normalizeGitLabApiUrl, } from "./utils/url.js";
|
|
94
|
+
import { encodeGitLabPath, encodeGitLabPathSegment, isCleartextRemoteUrl, normalizeGitLabApiUrl, } from "./utils/url.js";
|
|
97
95
|
import { estimateMergeCommitCount, filterDiffsByPatterns, openSafeOutputWriteStream, readSafeExistingFile, summarizeWebhookEvents, } from "./utils/helpers.js";
|
|
98
96
|
import { graphqlQueryContainsWriteOperation, graphqlQueryContainsDeleteOperation, } from "./utils/graphql-query.js";
|
|
99
97
|
import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
|
|
@@ -108,9 +106,13 @@ import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./u
|
|
|
108
106
|
import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
|
|
109
107
|
import { applyJmespathToToolResult, getJmespathSyntaxError, JMESPATH_TOOL_ARGUMENT, JMESPATH_TOOL_ARGUMENT_DESCRIPTION, omitJmespathArgument, readJmespathExpression, } from "./utils/jmespath-tool-result.js";
|
|
110
108
|
import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
|
|
111
|
-
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
112
109
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
113
|
-
import { allTools, readOnlyTools, destructiveTools, deleteTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
|
|
110
|
+
import { advertisesReadOnly, allTools, readOnlyTools, destructiveTools, deleteTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, findSmallestToolsetForTool, isToolsetFullyActive, listDiscoverableCategories, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
|
|
111
|
+
import { buildUnavailableToolMessage, isSkippedByDiscoverActivation, } from "./tools/discover-activation.js";
|
|
112
|
+
import { selectExposedTools } from "./tools/exposed-tools.js";
|
|
113
|
+
import { createListedToolsCache, decorateToolsForList, } from "./tools/listed-tools.js";
|
|
114
|
+
import { isExcludedBySlimProfile, shouldApplySlimToolProfile } from "./tools/tool-profile.js";
|
|
115
|
+
import { shouldCompactToolResult } from "./utils/compact-tool-allowlist.js";
|
|
114
116
|
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, ErasePipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
|
|
115
117
|
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateGroupMilestoneSchema, CreateRepositorySchema, CreateGroupSchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupMilestoneSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, EditGroupMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GitLabBlameEntrySchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetGroupMilestoneSchema, GetGroupMilestoneIssuesSchema, GetGroupMilestoneMergeRequestsSchema, GetGroupMilestoneBurndownEventsSchema, GetDeploymentSchema, CreateDeploymentSchema, UpdateDeploymentSchema, DeploymentApprovalSchema, ListDeploymentMergeRequestsSchema, GetEnvironmentSchema, UpdateEnvironmentSchema, StopEnvironmentSchema, StopStaleEnvironmentsSchema, DeleteReviewAppEnvironmentsSchema, PipelineTriggerIdSchema, ListPipelineTriggersSchema, CreatePipelineTriggerSchema, UpdatePipelineTriggerSchema, TriggerPipelineSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, PipelineJobControlSchema, GetPipelineSchema, GetPipelineVariablesSchema, UpdatePipelineMetadataSchema, DeletePipelineSchema, PipelineReportSchema, WaitForPipelineSchema, WaitForPipelineJobSchema, PlayPipelineJobsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
116
118
|
// Discussion Schemas
|
|
@@ -148,7 +150,7 @@ const MCP_PROMPTS = [
|
|
|
148
150
|
render: (args) => `Review merge request !${args.merge_request_iid ?? "<iid>"} in project ${args.project_id ?? "<project_id>"}:\n` +
|
|
149
151
|
`1. Call list_merge_request_changed_files to get the changed file list (use excluded_file_patterns for lockfiles/dist/generated files).\n` +
|
|
150
152
|
`2. Call get_merge_request_file_diff in batches of 3-5 files, prioritizing source over config/tests.\n` +
|
|
151
|
-
`3. Leave findings with create_merge_request_thread (
|
|
153
|
+
`3. Leave inline findings with create_merge_request_thread (position: new_path/new_line). For a general note, call discover_tools with category "merge_requests", then create_merge_request_note.\n` +
|
|
152
154
|
`4. Summarize the review verdict in a final top-level note.`,
|
|
153
155
|
},
|
|
154
156
|
{
|
|
@@ -178,6 +180,20 @@ const MCP_PROMPTS = [
|
|
|
178
180
|
`3. Propose a remediation order, calling out any critical/high items with no available fix.`,
|
|
179
181
|
},
|
|
180
182
|
];
|
|
183
|
+
/**
|
|
184
|
+
* Error text for a call to a tool that tools/list omits.
|
|
185
|
+
* Suggests `discover_tools` only when that call would add the tool.
|
|
186
|
+
* Regex-denied tools stay blocked, and slim-excluded tools are not activated.
|
|
187
|
+
*/
|
|
188
|
+
function buildToolUnavailableMessage(toolName) {
|
|
189
|
+
return buildUnavailableToolMessage({
|
|
190
|
+
toolName,
|
|
191
|
+
toolset: findSmallestToolsetForTool(toolName),
|
|
192
|
+
deniedByRegex: GITLAB_DENIED_TOOLS_REGEX?.test(toolName) === true,
|
|
193
|
+
excludedBySlimProfile: isExcludedFromToolsetListing(toolName),
|
|
194
|
+
enabledByCurrentToolsets: isToolInEnabledToolset(toolName, enabledToolsets),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
181
197
|
/**
|
|
182
198
|
* Create a new MCP Server instance with request handlers registered.
|
|
183
199
|
* Each transport connection gets its own Server instance to prevent
|
|
@@ -185,44 +201,26 @@ const MCP_PROMPTS = [
|
|
|
185
201
|
*/
|
|
186
202
|
/** Builds an MCP server with the tool list filtered by toolsets, permission mode, and policy. */
|
|
187
203
|
function createServer() {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
let filteredTools = GITLAB_DENIED_TOOLS_REGEX
|
|
209
|
-
? toolsAfterReadOnly.filter(tool => !GITLAB_DENIED_TOOLS_REGEX.test(tool.name))
|
|
210
|
-
: [...toolsAfterReadOnly];
|
|
211
|
-
// Step 5.5: Always include discover_tools meta-tool (bypasses toolset filter)
|
|
212
|
-
const discoverTool = allTools.find(t => t.name === "discover_tools");
|
|
213
|
-
const filteredToolNames = new Set(filteredTools.map(t => t.name));
|
|
214
|
-
if (discoverTool && !filteredToolNames.has("discover_tools")) {
|
|
215
|
-
// Respect permission mode and regex denial filters
|
|
216
|
-
const passesPermissionMode = isToolAllowedByPermissionMode("discover_tools");
|
|
217
|
-
const passesRegex = !GITLAB_DENIED_TOOLS_REGEX?.test("discover_tools");
|
|
218
|
-
if (passesPermissionMode && passesRegex) {
|
|
219
|
-
filteredTools.push(discoverTool);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
// Step 5.7: Remove hidden policy tools
|
|
223
|
-
if (hiddenToolSet.size > 0) {
|
|
224
|
-
filteredTools = filteredTools.filter(tool => !hiddenToolSet.has(tool.name));
|
|
225
|
-
}
|
|
204
|
+
const filteredTools = selectExposedTools({
|
|
205
|
+
tools: allTools,
|
|
206
|
+
isInEnabledToolset: toolName => isToolInEnabledToolset(toolName, enabledToolsets),
|
|
207
|
+
individuallyEnabledTools,
|
|
208
|
+
featureFlagOverrides,
|
|
209
|
+
isAllowedByPermissionMode: isToolAllowedByPermissionMode,
|
|
210
|
+
deniedToolsRegex: GITLAB_DENIED_TOOLS_REGEX,
|
|
211
|
+
hiddenToolNames: hiddenToolSet,
|
|
212
|
+
applySlimProfile,
|
|
213
|
+
});
|
|
214
|
+
const listedToolDecoration = {
|
|
215
|
+
readOnlyToolNames: new Set([...readOnlyTools].filter(toolName => advertisesReadOnly(toolName))),
|
|
216
|
+
destructiveToolNames: destructiveTools,
|
|
217
|
+
approveToolNames: approveToolSet,
|
|
218
|
+
jmespathArgument: JMESPATH_TOOL_ARGUMENT,
|
|
219
|
+
jmespathArgumentDescription: JMESPATH_TOOL_ARGUMENT_DESCRIPTION,
|
|
220
|
+
};
|
|
221
|
+
const listedToolsCache = { revision: 0 };
|
|
222
|
+
const decorateListedTools = (tools) => decorateToolsForList(tools, listedToolDecoration);
|
|
223
|
+
const getListedTools = createListedToolsCache(decorateListedTools);
|
|
226
224
|
const mcpServer = new McpServer({
|
|
227
225
|
name: SERVER_NAME,
|
|
228
226
|
version: SERVER_VERSION,
|
|
@@ -261,57 +259,8 @@ function createServer() {
|
|
|
261
259
|
};
|
|
262
260
|
});
|
|
263
261
|
mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
264
|
-
// Step 6: Gemini $schema cleanup + annotations (only dynamic step per request)
|
|
265
|
-
// <<< START: Remove $schema for Gemini compatibility >>>
|
|
266
|
-
const tools = filteredTools.map(tool => {
|
|
267
|
-
const modified = { ...tool };
|
|
268
|
-
// Safety net: remove $schema if present (toJSONSchema strips it for zod schemas,
|
|
269
|
-
// but manually-defined schemas like discover_tools may still have it)
|
|
270
|
-
if (modified.inputSchema &&
|
|
271
|
-
typeof modified.inputSchema === "object" &&
|
|
272
|
-
modified.inputSchema !== null) {
|
|
273
|
-
if ("$schema" in modified.inputSchema) {
|
|
274
|
-
modified.inputSchema = { ...modified.inputSchema };
|
|
275
|
-
delete modified.inputSchema.$schema;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// Add MCP tool annotations
|
|
279
|
-
modified.annotations = {
|
|
280
|
-
...(readOnlyTools.has(tool.name) ? { readOnlyHint: true } : {}),
|
|
281
|
-
...(destructiveTools.has(tool.name) ? { destructiveHint: true } : {}),
|
|
282
|
-
...(approveToolSet.has(tool.name) ? { confirmationHint: true } : {}),
|
|
283
|
-
openWorldHint: true,
|
|
284
|
-
};
|
|
285
|
-
// Inject _confirmed optional parameter for approve-policy tools
|
|
286
|
-
if (approveToolSet.has(tool.name) && modified.inputSchema?.properties) {
|
|
287
|
-
modified.inputSchema = {
|
|
288
|
-
...modified.inputSchema,
|
|
289
|
-
properties: {
|
|
290
|
-
...modified.inputSchema.properties,
|
|
291
|
-
_confirmed: {
|
|
292
|
-
type: "boolean",
|
|
293
|
-
description: "Set to true to confirm execution of this approval-required tool.",
|
|
294
|
-
},
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
if (modified.inputSchema && typeof modified.inputSchema === "object") {
|
|
299
|
-
const properties = modified.inputSchema.properties && typeof modified.inputSchema.properties === "object"
|
|
300
|
-
? { ...modified.inputSchema.properties }
|
|
301
|
-
: {};
|
|
302
|
-
if (!(JMESPATH_TOOL_ARGUMENT in properties)) {
|
|
303
|
-
properties[JMESPATH_TOOL_ARGUMENT] = {
|
|
304
|
-
type: "string",
|
|
305
|
-
description: JMESPATH_TOOL_ARGUMENT_DESCRIPTION,
|
|
306
|
-
};
|
|
307
|
-
modified.inputSchema = { ...modified.inputSchema, properties };
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return modified;
|
|
311
|
-
});
|
|
312
|
-
// <<< END: Remove $schema for Gemini compatibility >>>
|
|
313
262
|
return {
|
|
314
|
-
tools,
|
|
263
|
+
tools: getListedTools(filteredTools, listedToolsCache.revision),
|
|
315
264
|
};
|
|
316
265
|
});
|
|
317
266
|
mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -330,9 +279,7 @@ function createServer() {
|
|
|
330
279
|
(toolName === "download_release_asset" && !IS_REMOTE);
|
|
331
280
|
const filtered = applyJmespathToToolResult(result, jmespathExpression, {
|
|
332
281
|
...completionOptions,
|
|
333
|
-
maskValue: maskingEngine
|
|
334
|
-
? (value) => maskingEngine.maskValue(value)
|
|
335
|
-
: undefined,
|
|
282
|
+
maskValue: maskingEngine ? (value) => maskingEngine.maskValue(value) : undefined,
|
|
336
283
|
});
|
|
337
284
|
const masked = maskingEngine
|
|
338
285
|
? maskingEngine.maskToolResult(filtered, {
|
|
@@ -341,7 +288,7 @@ function createServer() {
|
|
|
341
288
|
: filtered;
|
|
342
289
|
return compactMcpToolResult({
|
|
343
290
|
result: masked,
|
|
344
|
-
enabled: GITLAB_MCP_COMPACT_RESULTS,
|
|
291
|
+
enabled: shouldCompactToolResult(toolName, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_TOOL_NAMES),
|
|
345
292
|
maxChars: GITLAB_MCP_COMPACT_RESULT_CHARS,
|
|
346
293
|
toolName,
|
|
347
294
|
args: readOwnRecord(request.params.arguments),
|
|
@@ -356,7 +303,7 @@ function createServer() {
|
|
|
356
303
|
durationMs,
|
|
357
304
|
error: safeError instanceof Error ? safeError.message : String(safeError),
|
|
358
305
|
}, `tool_call_error: ${toolName} (${durationMs}ms)`);
|
|
359
|
-
|
|
306
|
+
return safeError;
|
|
360
307
|
};
|
|
361
308
|
try {
|
|
362
309
|
const authData = (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) && sessionId
|
|
@@ -418,25 +365,34 @@ function createServer() {
|
|
|
418
365
|
if (isToolAllowedByPermissionMode(toolName) &&
|
|
419
366
|
!hiddenAndNotDenied &&
|
|
420
367
|
!filteredTools.some(t => t.name === toolName)) {
|
|
421
|
-
throw new Error(
|
|
368
|
+
throw new Error(buildToolUnavailableMessage(toolName));
|
|
422
369
|
}
|
|
423
370
|
// Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
|
|
424
371
|
if (toolName === "discover_tools") {
|
|
425
372
|
const category = request.params.arguments?.category?.trim()?.toLowerCase();
|
|
426
373
|
const currentToolNames = new Set(filteredTools.map(t => t.name));
|
|
374
|
+
const isNotActivatable = (name) => isSkippedByDiscoverActivation({
|
|
375
|
+
allowedByPermissionMode: isToolAllowedByPermissionMode(name),
|
|
376
|
+
deniedByRegex: GITLAB_DENIED_TOOLS_REGEX?.test(name) === true,
|
|
377
|
+
hidden: hiddenToolSet.has(name),
|
|
378
|
+
excludedBySlimProfile: isExcludedFromToolsetListing(name),
|
|
379
|
+
});
|
|
427
380
|
if (!category) {
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
isDefault: def.isDefault,
|
|
434
|
-
}));
|
|
381
|
+
// Fully listed only. Partial overlap with `core` must stay inactive so
|
|
382
|
+
// clients still call discover_tools to add the missing tools. Tools this
|
|
383
|
+
// handler skips (slim profile, regex, permission mode, hidden) do not
|
|
384
|
+
// keep a toolset inactive.
|
|
385
|
+
const categories = listDiscoverableCategories(currentToolNames, isNotActivatable);
|
|
435
386
|
return logCompletion({
|
|
436
|
-
content: [
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
437
389
|
type: "text",
|
|
438
|
-
text: JSON.stringify({
|
|
439
|
-
|
|
390
|
+
text: JSON.stringify({
|
|
391
|
+
categories,
|
|
392
|
+
hint: "Call discover_tools with a category name to activate it",
|
|
393
|
+
}),
|
|
394
|
+
},
|
|
395
|
+
],
|
|
440
396
|
});
|
|
441
397
|
}
|
|
442
398
|
if (!ALL_TOOLSET_IDS.has(category)) {
|
|
@@ -457,8 +413,8 @@ function createServer() {
|
|
|
457
413
|
isError: true,
|
|
458
414
|
});
|
|
459
415
|
}
|
|
460
|
-
//
|
|
461
|
-
const alreadyActive =
|
|
416
|
+
// Same predicate as the category listing `active` flag.
|
|
417
|
+
const alreadyActive = isToolsetFullyActive(toolsetDef.tools, currentToolNames, isNotActivatable);
|
|
462
418
|
if (alreadyActive) {
|
|
463
419
|
return logCompletion({
|
|
464
420
|
content: [
|
|
@@ -476,11 +432,7 @@ function createServer() {
|
|
|
476
432
|
continue;
|
|
477
433
|
if (currentToolNames.has(tool.name))
|
|
478
434
|
continue;
|
|
479
|
-
if (
|
|
480
|
-
continue;
|
|
481
|
-
if (GITLAB_DENIED_TOOLS_REGEX?.test(tool.name))
|
|
482
|
-
continue;
|
|
483
|
-
if (hiddenToolSet.has(tool.name))
|
|
435
|
+
if (isNotActivatable(tool.name))
|
|
484
436
|
continue;
|
|
485
437
|
newTools.push(tool);
|
|
486
438
|
}
|
|
@@ -495,6 +447,7 @@ function createServer() {
|
|
|
495
447
|
});
|
|
496
448
|
}
|
|
497
449
|
filteredTools.push(...newTools);
|
|
450
|
+
listedToolsCache.revision += 1;
|
|
498
451
|
// Notify client that tool list has changed
|
|
499
452
|
try {
|
|
500
453
|
await mcpServer.server.sendToolListChanged();
|
|
@@ -505,14 +458,16 @@ function createServer() {
|
|
|
505
458
|
const addedNames = newTools.map(t => t.name);
|
|
506
459
|
logger.info({ event: "toolset_activated", category, toolCount: addedNames.length }, `Activated toolset: ${category} (+${addedNames.length} tools)`);
|
|
507
460
|
return logCompletion({
|
|
508
|
-
content: [
|
|
461
|
+
content: [
|
|
462
|
+
{
|
|
509
463
|
type: "text",
|
|
510
464
|
text: JSON.stringify({
|
|
511
465
|
activated: category,
|
|
512
466
|
addedTools: addedNames,
|
|
513
467
|
totalTools: filteredTools.length,
|
|
514
468
|
}),
|
|
515
|
-
}
|
|
469
|
+
},
|
|
470
|
+
],
|
|
516
471
|
});
|
|
517
472
|
}
|
|
518
473
|
// Check approve policy: tool is exposed but requires explicit confirmation
|
|
@@ -547,8 +502,20 @@ function createServer() {
|
|
|
547
502
|
return logCompletion(result);
|
|
548
503
|
}
|
|
549
504
|
catch (error) {
|
|
550
|
-
|
|
551
|
-
|
|
505
|
+
const isGitLabApiError = error instanceof GitLabApiError;
|
|
506
|
+
const safeError = logError(error);
|
|
507
|
+
if (isGitLabApiError) {
|
|
508
|
+
return {
|
|
509
|
+
content: [
|
|
510
|
+
{
|
|
511
|
+
type: "text",
|
|
512
|
+
text: safeError instanceof Error ? safeError.message : String(safeError),
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
isError: true,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
throw safeError;
|
|
552
519
|
}
|
|
553
520
|
});
|
|
554
521
|
return mcpServer;
|
|
@@ -557,7 +524,10 @@ function createServer() {
|
|
|
557
524
|
* Validate configuration at startup
|
|
558
525
|
*/
|
|
559
526
|
function isLoopbackBindHost(host) {
|
|
560
|
-
const normalized = host
|
|
527
|
+
const normalized = host
|
|
528
|
+
.trim()
|
|
529
|
+
.toLowerCase()
|
|
530
|
+
.replace(/^\[|\]$/g, "");
|
|
561
531
|
const isIpv4Loopback = /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
562
532
|
return (normalized === "localhost" ||
|
|
563
533
|
isIpv4Loopback ||
|
|
@@ -565,7 +535,10 @@ function isLoopbackBindHost(host) {
|
|
|
565
535
|
normalized === "0:0:0:0:0:0:0:1");
|
|
566
536
|
}
|
|
567
537
|
function formatHostWithPort(host, port) {
|
|
568
|
-
const normalized = host
|
|
538
|
+
const normalized = host
|
|
539
|
+
.trim()
|
|
540
|
+
.toLowerCase()
|
|
541
|
+
.replace(/^\[|\]$/g, "");
|
|
569
542
|
if (!normalized || normalized === "0.0.0.0" || normalized === "::")
|
|
570
543
|
return null;
|
|
571
544
|
if (normalized.includes(":"))
|
|
@@ -916,6 +889,12 @@ const hiddenToolSet = new Set((GITLAB_TOOL_POLICY_HIDDEN_RAW || "")
|
|
|
916
889
|
logger.warn({ event: "unknown_approve_tool", name }, `GITLAB_TOOL_POLICY_APPROVE contains unknown tool: "${name}"`);
|
|
917
890
|
}
|
|
918
891
|
}
|
|
892
|
+
if (GITLAB_MCP_COMPACT_TOOL_NAMES.size > 0) {
|
|
893
|
+
const unknownCompactTools = [...GITLAB_MCP_COMPACT_TOOL_NAMES].filter(name => !knownToolNames.has(name));
|
|
894
|
+
if (unknownCompactTools.length > 0) {
|
|
895
|
+
logger.warn(`Unknown tool names in GITLAB_MCP_COMPACT_TOOLS (will be ignored): ${unknownCompactTools.join(", ")}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
919
898
|
for (const name of hiddenToolSet) {
|
|
920
899
|
if (!knownToolNames.has(name)) {
|
|
921
900
|
logger.warn({ event: "unknown_hidden_tool", name }, `GITLAB_TOOL_POLICY_HIDDEN contains unknown tool: "${name}"`);
|
|
@@ -1097,12 +1076,15 @@ function buildAuthHeaders() {
|
|
|
1097
1076
|
// OAuth token takes priority over PAT when both are set.
|
|
1098
1077
|
// NOTE: Changed in PR #400 — previously GITLAB_JOB_TOKEN had highest priority.
|
|
1099
1078
|
// If both GITLAB_PERSONAL_ACCESS_TOKEN and GITLAB_JOB_TOKEN are set, PAT wins.
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1079
|
+
// OAuth tokens always travel as `Authorization: Bearer`, even with GITLAB_IS_OLD: the HTTP
|
|
1080
|
+
// client strips `Authorization` on cross-origin redirects but not `Private-Token`.
|
|
1081
|
+
if (OAUTH_ACCESS_TOKEN) {
|
|
1082
|
+
return { Authorization: `Bearer ${OAUTH_ACCESS_TOKEN}` };
|
|
1103
1083
|
}
|
|
1104
|
-
if (
|
|
1105
|
-
return
|
|
1084
|
+
if (GITLAB_PERSONAL_ACCESS_TOKEN) {
|
|
1085
|
+
return IS_OLD
|
|
1086
|
+
? { "Private-Token": String(GITLAB_PERSONAL_ACCESS_TOKEN) }
|
|
1087
|
+
: { Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}` };
|
|
1106
1088
|
}
|
|
1107
1089
|
// Fall back to CI job token
|
|
1108
1090
|
if (GITLAB_JOB_TOKEN) {
|
|
@@ -1212,6 +1194,13 @@ const downloadRedirectOptions = () => ({
|
|
|
1212
1194
|
const enabledToolsets = parseEnabledToolsets(GITLAB_TOOLSETS_RAW);
|
|
1213
1195
|
const individuallyEnabledTools = parseIndividualTools(GITLAB_TOOLS_RAW);
|
|
1214
1196
|
const featureFlagOverrides = buildFeatureFlagOverrides();
|
|
1197
|
+
const applySlimProfile = shouldApplySlimToolProfile(GITLAB_TOOL_PROFILE, GITLAB_TOOLSETS_RAW);
|
|
1198
|
+
if (GITLAB_TOOL_PROFILE === "slim" && !applySlimProfile) {
|
|
1199
|
+
logger.warn("GITLAB_TOOL_PROFILE=slim is ignored because GITLAB_TOOLSETS is set");
|
|
1200
|
+
}
|
|
1201
|
+
function isExcludedFromToolsetListing(toolName) {
|
|
1202
|
+
return isExcludedBySlimProfile(toolName, applySlimProfile, individuallyEnabledTools);
|
|
1203
|
+
}
|
|
1215
1204
|
// Warn about potentially confusing configuration
|
|
1216
1205
|
if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
|
|
1217
1206
|
logger.warn("GITLAB_TOOLSETS is set alongside legacy flags (USE_PIPELINE, USE_MILESTONE, USE_GITLAB_WIKI). " +
|
|
@@ -1325,6 +1314,12 @@ if (!isCliInvocation(process.argv) &&
|
|
|
1325
1314
|
logger.info("Either set GITLAB_PERSONAL_ACCESS_TOKEN or enable OAuth with GITLAB_USE_OAUTH=true");
|
|
1326
1315
|
process.exit(1);
|
|
1327
1316
|
}
|
|
1317
|
+
class GitLabApiError extends Error {
|
|
1318
|
+
constructor(message) {
|
|
1319
|
+
super(message);
|
|
1320
|
+
this.name = "GitLabApiError";
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1328
1323
|
/**
|
|
1329
1324
|
* Utility function for handling GitLab API errors
|
|
1330
1325
|
* API 에러 처리를 위한 유틸리티 함수 (Utility function for handling API errors)
|
|
@@ -1341,12 +1336,12 @@ async function handleGitLabError(response) {
|
|
|
1341
1336
|
// before the common tool-call boundary returns/logs it.
|
|
1342
1337
|
logger.error("GitLab API Rate Limit Exceeded");
|
|
1343
1338
|
logger.error("User API Key Rate limit exceeded. Please try again later.");
|
|
1344
|
-
const error = new
|
|
1339
|
+
const error = new GitLabApiError(`GitLab API Rate Limit Exceeded: ${errorBody}`);
|
|
1345
1340
|
throw error;
|
|
1346
1341
|
}
|
|
1347
1342
|
else {
|
|
1348
1343
|
// Handle other API errors
|
|
1349
|
-
const error = new
|
|
1344
|
+
const error = new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
1350
1345
|
throw error;
|
|
1351
1346
|
}
|
|
1352
1347
|
}
|
|
@@ -1411,7 +1406,7 @@ async function forkProject(projectId, namespace) {
|
|
|
1411
1406
|
});
|
|
1412
1407
|
// Handle case where project already exists
|
|
1413
1408
|
if (response.status === 409) {
|
|
1414
|
-
throw new
|
|
1409
|
+
throw new GitLabApiError("Project already exists in the target namespace");
|
|
1415
1410
|
}
|
|
1416
1411
|
await handleGitLabError(response);
|
|
1417
1412
|
const data = await response.json();
|
|
@@ -1482,7 +1477,7 @@ async function getFileContents(projectId, filePath, ref) {
|
|
|
1482
1477
|
});
|
|
1483
1478
|
// Handle file not found
|
|
1484
1479
|
if (response.status === 404) {
|
|
1485
|
-
throw new
|
|
1480
|
+
throw new GitLabApiError(`File not found: ${filePath}`);
|
|
1486
1481
|
}
|
|
1487
1482
|
await handleGitLabError(response);
|
|
1488
1483
|
const data = await response.json();
|
|
@@ -1515,7 +1510,7 @@ async function createIssue(projectId, options) {
|
|
|
1515
1510
|
// Handle bad request
|
|
1516
1511
|
if (response.status === 400) {
|
|
1517
1512
|
const errorBody = await response.text();
|
|
1518
|
-
throw new
|
|
1513
|
+
throw new GitLabApiError(`Invalid request: ${errorBody}`);
|
|
1519
1514
|
}
|
|
1520
1515
|
await handleGitLabError(response);
|
|
1521
1516
|
const data = await response.json();
|
|
@@ -1751,7 +1746,7 @@ async function executeGraphQL(query, variables = {}) {
|
|
|
1751
1746
|
});
|
|
1752
1747
|
if (!response.ok) {
|
|
1753
1748
|
const errorBody = await response.text();
|
|
1754
|
-
throw new
|
|
1749
|
+
throw new GitLabApiError(`GraphQL request failed (${response.status}): ${errorBody}`);
|
|
1755
1750
|
}
|
|
1756
1751
|
const json = await response.json();
|
|
1757
1752
|
if (json.errors && json.errors.length > 0) {
|
|
@@ -1790,7 +1785,9 @@ async function resolveNamesToIds(projectPath, namespaceKind, labelNames, usernam
|
|
|
1790
1785
|
// One alias per label — exact title match via the `title` argument, includes ancestor
|
|
1791
1786
|
// group labels, single round trip with no pagination needed.
|
|
1792
1787
|
const varDefs = labelNames.map((_, i) => `$l${i}: String!`).join(", ");
|
|
1793
|
-
const aliases = labelNames
|
|
1788
|
+
const aliases = labelNames
|
|
1789
|
+
.map((_, i) => `l${i}: labels(title: $l${i}, includeAncestorGroups: true, first: 1) { nodes { id } }`)
|
|
1790
|
+
.join(" ");
|
|
1794
1791
|
const rootField = namespaceKind === "group" ? "group" : "project";
|
|
1795
1792
|
const data = await executeGraphQL(`query($path: ID!, $usernames: [String!]!${varDefs ? `, ${varDefs}` : ""}) {
|
|
1796
1793
|
${rootField}(fullPath: $path) { ${aliases || "__typename"} }
|
|
@@ -3200,11 +3197,11 @@ async function createMergeRequest(projectId, options) {
|
|
|
3200
3197
|
});
|
|
3201
3198
|
if (response.status === 400) {
|
|
3202
3199
|
const errorBody = await response.text();
|
|
3203
|
-
throw new
|
|
3200
|
+
throw new GitLabApiError(`Invalid request: ${errorBody}`);
|
|
3204
3201
|
}
|
|
3205
3202
|
if (!response.ok) {
|
|
3206
3203
|
const errorBody = await response.text();
|
|
3207
|
-
throw new
|
|
3204
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
3208
3205
|
}
|
|
3209
3206
|
const data = await response.json();
|
|
3210
3207
|
return GitLabMergeRequestSchema.parse(data);
|
|
@@ -3292,7 +3289,7 @@ async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
|
|
|
3292
3289
|
});
|
|
3293
3290
|
if (!response.ok) {
|
|
3294
3291
|
const errorText = await response.text();
|
|
3295
|
-
throw new
|
|
3292
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
3296
3293
|
}
|
|
3297
3294
|
}
|
|
3298
3295
|
/**
|
|
@@ -3385,7 +3382,9 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
|
|
|
3385
3382
|
async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
|
|
3386
3383
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
3387
3384
|
const basePath = `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
|
|
3388
|
-
const url = new URL(discussionId
|
|
3385
|
+
const url = new URL(discussionId
|
|
3386
|
+
? `${basePath}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`
|
|
3387
|
+
: `${basePath}/notes`);
|
|
3389
3388
|
const payload = { body };
|
|
3390
3389
|
if (createdAt) {
|
|
3391
3390
|
payload.created_at = createdAt;
|
|
@@ -3452,7 +3451,7 @@ async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
|
|
|
3452
3451
|
});
|
|
3453
3452
|
if (!response.ok) {
|
|
3454
3453
|
const errorText = await response.text();
|
|
3455
|
-
throw new
|
|
3454
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
3456
3455
|
}
|
|
3457
3456
|
}
|
|
3458
3457
|
// --- Emoji Reactions (REST) ---
|
|
@@ -3623,7 +3622,7 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
|
|
|
3623
3622
|
});
|
|
3624
3623
|
if (!response.ok) {
|
|
3625
3624
|
const errorBody = await response.text();
|
|
3626
|
-
throw new
|
|
3625
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
3627
3626
|
}
|
|
3628
3627
|
const data = await response.json();
|
|
3629
3628
|
return GitLabCreateUpdateFileResponseSchema.parse(data);
|
|
@@ -3652,11 +3651,11 @@ async function createCommit(projectId, message, branch, actions) {
|
|
|
3652
3651
|
});
|
|
3653
3652
|
if (response.status === 400) {
|
|
3654
3653
|
const errorBody = await response.text();
|
|
3655
|
-
throw new
|
|
3654
|
+
throw new GitLabApiError(`Invalid request: ${errorBody}`);
|
|
3656
3655
|
}
|
|
3657
3656
|
if (!response.ok) {
|
|
3658
3657
|
const errorBody = await response.text();
|
|
3659
|
-
throw new
|
|
3658
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
3660
3659
|
}
|
|
3661
3660
|
const data = await response.json();
|
|
3662
3661
|
return GitLabCommitSchema.parse(data);
|
|
@@ -3682,7 +3681,7 @@ async function searchProjects(query, page = 1, perPage = 20) {
|
|
|
3682
3681
|
});
|
|
3683
3682
|
if (!response.ok) {
|
|
3684
3683
|
const errorBody = await response.text();
|
|
3685
|
-
throw new
|
|
3684
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
3686
3685
|
}
|
|
3687
3686
|
const unfiltered = (await response.json());
|
|
3688
3687
|
const projects = filterProjectsByAllowlist(unfiltered);
|
|
@@ -3769,7 +3768,7 @@ async function createRepository(options) {
|
|
|
3769
3768
|
});
|
|
3770
3769
|
if (!response.ok) {
|
|
3771
3770
|
const errorBody = await response.text();
|
|
3772
|
-
throw new
|
|
3771
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
3773
3772
|
}
|
|
3774
3773
|
const data = await response.json();
|
|
3775
3774
|
return GitLabRepositorySchema.parse(data);
|
|
@@ -4195,7 +4194,7 @@ async function getBranchDiffs(projectId, from, to, straight) {
|
|
|
4195
4194
|
});
|
|
4196
4195
|
if (!response.ok) {
|
|
4197
4196
|
const errorBody = await response.text();
|
|
4198
|
-
throw new
|
|
4197
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
4199
4198
|
}
|
|
4200
4199
|
const data = await response.json();
|
|
4201
4200
|
return GitLabCompareResultSchema.parse(data);
|
|
@@ -4428,7 +4427,7 @@ noteableIid, body) {
|
|
|
4428
4427
|
});
|
|
4429
4428
|
if (!response.ok) {
|
|
4430
4429
|
const errorText = await response.text();
|
|
4431
|
-
throw new
|
|
4430
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
4432
4431
|
}
|
|
4433
4432
|
return await response.json();
|
|
4434
4433
|
}
|
|
@@ -4443,7 +4442,7 @@ async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
|
|
|
4443
4442
|
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/merge_requests/${encodeGitLabPathSegment(merge_request_iid)}/draft_notes/${encodeGitLabPathSegment(draft_note_id)}`, { ...getFetchConfig() });
|
|
4444
4443
|
if (!response.ok) {
|
|
4445
4444
|
const errorText = await response.text();
|
|
4446
|
-
throw new
|
|
4445
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
4447
4446
|
}
|
|
4448
4447
|
const data = await response.json();
|
|
4449
4448
|
return GitLabDraftNoteSchema.parse(data);
|
|
@@ -4457,7 +4456,7 @@ async function listDraftNotes(projectId, mergeRequestIid) {
|
|
|
4457
4456
|
});
|
|
4458
4457
|
if (!response.ok) {
|
|
4459
4458
|
const errorText = await response.text();
|
|
4460
|
-
throw new
|
|
4459
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
4461
4460
|
}
|
|
4462
4461
|
const data = await response.json();
|
|
4463
4462
|
return z.array(GitLabDraftNoteSchema).parse(data);
|
|
@@ -4492,7 +4491,7 @@ async function createDraftNote(projectId, mergeRequestIid, body, inReplyToDiscus
|
|
|
4492
4491
|
});
|
|
4493
4492
|
if (!response.ok) {
|
|
4494
4493
|
const errorText = await response.text();
|
|
4495
|
-
throw new
|
|
4494
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
4496
4495
|
}
|
|
4497
4496
|
const data = await response.json();
|
|
4498
4497
|
return GitLabDraftNoteSchema.parse(data);
|
|
@@ -4527,7 +4526,7 @@ async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, po
|
|
|
4527
4526
|
});
|
|
4528
4527
|
if (!response.ok) {
|
|
4529
4528
|
const errorText = await response.text();
|
|
4530
|
-
throw new
|
|
4529
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
4531
4530
|
}
|
|
4532
4531
|
const data = await response.json();
|
|
4533
4532
|
return GitLabDraftNoteSchema.parse(data);
|
|
@@ -4548,7 +4547,7 @@ async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
|
4548
4547
|
});
|
|
4549
4548
|
if (!response.ok) {
|
|
4550
4549
|
const errorText = await response.text();
|
|
4551
|
-
throw new
|
|
4550
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
4552
4551
|
}
|
|
4553
4552
|
}
|
|
4554
4553
|
/**
|
|
@@ -4567,7 +4566,7 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
|
4567
4566
|
});
|
|
4568
4567
|
if (!response.ok) {
|
|
4569
4568
|
const errorText = await response.text();
|
|
4570
|
-
throw new
|
|
4569
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
4571
4570
|
}
|
|
4572
4571
|
// Handle empty response (204 No Content) or successful response
|
|
4573
4572
|
const responseText = await response.text();
|
|
@@ -4648,7 +4647,7 @@ async function bulkPublishDraftNotes(projectId, mergeRequestIid, options = {}) {
|
|
|
4648
4647
|
});
|
|
4649
4648
|
if (!response.ok) {
|
|
4650
4649
|
const errorText = await response.text();
|
|
4651
|
-
throw new
|
|
4650
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
4652
4651
|
}
|
|
4653
4652
|
// Handle empty response (204 No Content) or successful response
|
|
4654
4653
|
const responseText = await response.text();
|
|
@@ -4679,7 +4678,7 @@ async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionI
|
|
|
4679
4678
|
});
|
|
4680
4679
|
if (!response.ok) {
|
|
4681
4680
|
const errorText = await response.text();
|
|
4682
|
-
throw new
|
|
4681
|
+
throw new GitLabApiError(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
4683
4682
|
}
|
|
4684
4683
|
}
|
|
4685
4684
|
/**
|
|
@@ -5271,7 +5270,7 @@ async function getPipeline(projectId, pipelineId, signal) {
|
|
|
5271
5270
|
signal,
|
|
5272
5271
|
});
|
|
5273
5272
|
if (response.status === 404) {
|
|
5274
|
-
throw new
|
|
5273
|
+
throw new GitLabApiError(`Pipeline not found`);
|
|
5275
5274
|
}
|
|
5276
5275
|
await handleGitLabError(response);
|
|
5277
5276
|
const data = await response.json();
|
|
@@ -5344,7 +5343,7 @@ async function getDeployment(projectId, deploymentId) {
|
|
|
5344
5343
|
...getFetchConfig(),
|
|
5345
5344
|
});
|
|
5346
5345
|
if (response.status === 404) {
|
|
5347
|
-
throw new
|
|
5346
|
+
throw new GitLabApiError(`Deployment not found`);
|
|
5348
5347
|
}
|
|
5349
5348
|
await handleGitLabError(response);
|
|
5350
5349
|
const data = await response.json();
|
|
@@ -5358,7 +5357,8 @@ async function deploymentRequest(projectId, path = "", method = "GET", body, que
|
|
|
5358
5357
|
url.searchParams.set(key, String(value));
|
|
5359
5358
|
}
|
|
5360
5359
|
const response = await fetch(url.toString(), {
|
|
5361
|
-
...getFetchConfig(),
|
|
5360
|
+
...getFetchConfig(),
|
|
5361
|
+
method,
|
|
5362
5362
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
5363
5363
|
});
|
|
5364
5364
|
await handleGitLabError(response);
|
|
@@ -5396,7 +5396,7 @@ async function getEnvironment(projectId, environmentId) {
|
|
|
5396
5396
|
...getFetchConfig(),
|
|
5397
5397
|
});
|
|
5398
5398
|
if (response.status === 404) {
|
|
5399
|
-
throw new
|
|
5399
|
+
throw new GitLabApiError(`Environment not found`);
|
|
5400
5400
|
}
|
|
5401
5401
|
await handleGitLabError(response);
|
|
5402
5402
|
const data = await response.json();
|
|
@@ -5408,7 +5408,11 @@ async function environmentRequest(projectId, path, method, body, query) {
|
|
|
5408
5408
|
for (const [key, value] of Object.entries(query ?? {}))
|
|
5409
5409
|
if (value !== undefined)
|
|
5410
5410
|
url.searchParams.set(key, String(value));
|
|
5411
|
-
const response = await fetch(url.toString(), {
|
|
5411
|
+
const response = await fetch(url.toString(), {
|
|
5412
|
+
...getFetchConfig(),
|
|
5413
|
+
method,
|
|
5414
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
5415
|
+
});
|
|
5412
5416
|
await handleGitLabError(response);
|
|
5413
5417
|
return response.status === 204 ? { success: true } : response.json();
|
|
5414
5418
|
}
|
|
@@ -5428,7 +5432,11 @@ async function triggerPipeline(projectId, token, ref, variables, inputs) {
|
|
|
5428
5432
|
body.variables = variables;
|
|
5429
5433
|
if (inputs)
|
|
5430
5434
|
body.inputs = inputs;
|
|
5431
|
-
const response = await fetch(url.toString(), {
|
|
5435
|
+
const response = await fetch(url.toString(), {
|
|
5436
|
+
...getFetchConfig(),
|
|
5437
|
+
method: "POST",
|
|
5438
|
+
body: Object.keys(body).length ? JSON.stringify(body) : undefined,
|
|
5439
|
+
});
|
|
5432
5440
|
await handleGitLabError(response);
|
|
5433
5441
|
return response.json();
|
|
5434
5442
|
}
|
|
@@ -5449,7 +5457,7 @@ async function listPipelineJobs(projectId, pipelineId, options = {}) {
|
|
|
5449
5457
|
...getFetchConfig(),
|
|
5450
5458
|
});
|
|
5451
5459
|
if (response.status === 404) {
|
|
5452
|
-
throw new
|
|
5460
|
+
throw new GitLabApiError(`Pipeline not found`);
|
|
5453
5461
|
}
|
|
5454
5462
|
await handleGitLabError(response);
|
|
5455
5463
|
const data = await response.json();
|
|
@@ -5472,7 +5480,7 @@ async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
|
|
|
5472
5480
|
...getFetchConfig(),
|
|
5473
5481
|
});
|
|
5474
5482
|
if (response.status === 404) {
|
|
5475
|
-
throw new
|
|
5483
|
+
throw new GitLabApiError(`Pipeline not found`);
|
|
5476
5484
|
}
|
|
5477
5485
|
await handleGitLabError(response);
|
|
5478
5486
|
const data = await response.json();
|
|
@@ -5486,7 +5494,7 @@ async function getPipelineJob(projectId, jobId, signal) {
|
|
|
5486
5494
|
signal,
|
|
5487
5495
|
});
|
|
5488
5496
|
if (response.status === 404) {
|
|
5489
|
-
throw new
|
|
5497
|
+
throw new GitLabApiError(`Job not found`);
|
|
5490
5498
|
}
|
|
5491
5499
|
await handleGitLabError(response);
|
|
5492
5500
|
const data = await response.json();
|
|
@@ -5514,7 +5522,7 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
|
|
|
5514
5522
|
},
|
|
5515
5523
|
});
|
|
5516
5524
|
if (response.status === 404) {
|
|
5517
|
-
throw new
|
|
5525
|
+
throw new GitLabApiError(`Job trace not found or job is not finished yet`);
|
|
5518
5526
|
}
|
|
5519
5527
|
await handleGitLabError(response);
|
|
5520
5528
|
const fullTrace = await response.text();
|
|
@@ -5576,7 +5584,7 @@ async function listJobArtifacts(projectId, jobId, options = {}) {
|
|
|
5576
5584
|
...getFetchConfig(),
|
|
5577
5585
|
});
|
|
5578
5586
|
if (response.status === 404) {
|
|
5579
|
-
throw new
|
|
5587
|
+
throw new GitLabApiError(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
|
|
5580
5588
|
}
|
|
5581
5589
|
await handleGitLabError(response);
|
|
5582
5590
|
const data = await response.json();
|
|
@@ -5596,7 +5604,7 @@ async function downloadJobArtifacts(projectId, jobId, localPath) {
|
|
|
5596
5604
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts`);
|
|
5597
5605
|
const response = await fetchWithValidatedRedirects(url.toString(), downloadRedirectOptions());
|
|
5598
5606
|
if (response.status === 404) {
|
|
5599
|
-
throw new
|
|
5607
|
+
throw new GitLabApiError(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
|
|
5600
5608
|
}
|
|
5601
5609
|
await handleGitLabError(response);
|
|
5602
5610
|
const filename = `artifacts_job_${encodeGitLabPathSegment(jobId)}.zip`;
|
|
@@ -5622,7 +5630,7 @@ async function getJobArtifactFile(projectId, jobId, artifactPath) {
|
|
|
5622
5630
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/${encodedArtifactPath}`);
|
|
5623
5631
|
const response = await fetchWithValidatedRedirects(url.toString(), downloadRedirectOptions());
|
|
5624
5632
|
if (response.status === 404) {
|
|
5625
|
-
throw new
|
|
5633
|
+
throw new GitLabApiError(`Artifact file not found: ${artifactPath}`);
|
|
5626
5634
|
}
|
|
5627
5635
|
await handleGitLabError(response);
|
|
5628
5636
|
return await response.text();
|
|
@@ -6038,10 +6046,10 @@ async function getRepositoryTree(options) {
|
|
|
6038
6046
|
queryParams.append("pagination", options.pagination);
|
|
6039
6047
|
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(options.project_id))}/repository/tree?${queryParams.toString()}`, { ...getFetchConfig() });
|
|
6040
6048
|
if (response.status === 404) {
|
|
6041
|
-
throw new
|
|
6049
|
+
throw new GitLabApiError("Repository or path not found");
|
|
6042
6050
|
}
|
|
6043
6051
|
if (!response.ok) {
|
|
6044
|
-
throw new
|
|
6052
|
+
throw new GitLabApiError(`Failed to get repository tree: ${response.statusText}`);
|
|
6045
6053
|
}
|
|
6046
6054
|
const data = await response.json();
|
|
6047
6055
|
const items = z.array(GitLabTreeItemSchema).parse(data);
|
|
@@ -7999,8 +8007,7 @@ async function handleToolCall(params) {
|
|
|
7999
8007
|
}
|
|
8000
8008
|
case "push_files": {
|
|
8001
8009
|
const args = PushFilesSchema.parse(params.arguments);
|
|
8002
|
-
if (GITLAB_PERMISSION_MODE === "modify" &&
|
|
8003
|
-
fileOperationsIncludeDeleteOrMove(args.files)) {
|
|
8010
|
+
if (GITLAB_PERMISSION_MODE === "modify" && fileOperationsIncludeDeleteOrMove(args.files)) {
|
|
8004
8011
|
throw new Error("push_files does not allow delete or move actions in modify mode");
|
|
8005
8012
|
}
|
|
8006
8013
|
const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(f => ({
|
|
@@ -8871,7 +8878,14 @@ async function handleToolCall(params) {
|
|
|
8871
8878
|
const args = DeleteWorkItemEmojiReactionSchema.parse(params.arguments);
|
|
8872
8879
|
const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
|
|
8873
8880
|
const result = await removeGraphQLAwardEmoji(workItemGID, args.name);
|
|
8874
|
-
return {
|
|
8881
|
+
return {
|
|
8882
|
+
content: [
|
|
8883
|
+
{
|
|
8884
|
+
type: "text",
|
|
8885
|
+
text: JSON.stringify(result ?? { status: "success", message: "Work item emoji reaction removed" }),
|
|
8886
|
+
},
|
|
8887
|
+
],
|
|
8888
|
+
};
|
|
8875
8889
|
}
|
|
8876
8890
|
case "create_work_item_note_emoji_reaction": {
|
|
8877
8891
|
const args = CreateWorkItemNoteEmojiReactionSchema.parse(params.arguments);
|
|
@@ -8881,7 +8895,14 @@ async function handleToolCall(params) {
|
|
|
8881
8895
|
case "delete_work_item_note_emoji_reaction": {
|
|
8882
8896
|
const args = DeleteWorkItemNoteEmojiReactionSchema.parse(params.arguments);
|
|
8883
8897
|
const result = await removeGraphQLAwardEmoji(args.note_id, args.name);
|
|
8884
|
-
return {
|
|
8898
|
+
return {
|
|
8899
|
+
content: [
|
|
8900
|
+
{
|
|
8901
|
+
type: "text",
|
|
8902
|
+
text: JSON.stringify(result ?? { status: "success", message: "Work item note emoji reaction removed" }),
|
|
8903
|
+
},
|
|
8904
|
+
],
|
|
8905
|
+
};
|
|
8885
8906
|
}
|
|
8886
8907
|
case "get_timeline_events": {
|
|
8887
8908
|
const args = GetTimelineEventsSchema.parse(params.arguments);
|
|
@@ -9084,23 +9105,55 @@ async function handleToolCall(params) {
|
|
|
9084
9105
|
}
|
|
9085
9106
|
case "get_pipeline_variables": {
|
|
9086
9107
|
const { project_id, pipeline_id, page, per_page } = GetPipelineVariablesSchema.parse(params.arguments);
|
|
9087
|
-
return {
|
|
9108
|
+
return {
|
|
9109
|
+
content: [
|
|
9110
|
+
{
|
|
9111
|
+
type: "text",
|
|
9112
|
+
text: JSON.stringify(await getPipelineVariables(project_id, pipeline_id, page, per_page)),
|
|
9113
|
+
},
|
|
9114
|
+
],
|
|
9115
|
+
};
|
|
9088
9116
|
}
|
|
9089
9117
|
case "get_pipeline_test_report": {
|
|
9090
9118
|
const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
|
|
9091
|
-
return {
|
|
9119
|
+
return {
|
|
9120
|
+
content: [
|
|
9121
|
+
{
|
|
9122
|
+
type: "text",
|
|
9123
|
+
text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, false, page, per_page)),
|
|
9124
|
+
},
|
|
9125
|
+
],
|
|
9126
|
+
};
|
|
9092
9127
|
}
|
|
9093
9128
|
case "get_pipeline_test_report_summary": {
|
|
9094
9129
|
const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
|
|
9095
|
-
return {
|
|
9130
|
+
return {
|
|
9131
|
+
content: [
|
|
9132
|
+
{
|
|
9133
|
+
type: "text",
|
|
9134
|
+
text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, true, page, per_page)),
|
|
9135
|
+
},
|
|
9136
|
+
],
|
|
9137
|
+
};
|
|
9096
9138
|
}
|
|
9097
9139
|
case "delete_pipeline": {
|
|
9098
9140
|
const { project_id, pipeline_id } = DeletePipelineSchema.parse(params.arguments);
|
|
9099
|
-
return {
|
|
9141
|
+
return {
|
|
9142
|
+
content: [
|
|
9143
|
+
{ type: "text", text: JSON.stringify(await deletePipeline(project_id, pipeline_id)) },
|
|
9144
|
+
],
|
|
9145
|
+
};
|
|
9100
9146
|
}
|
|
9101
9147
|
case "update_pipeline_metadata": {
|
|
9102
9148
|
const { project_id, pipeline_id, name } = UpdatePipelineMetadataSchema.parse(params.arguments);
|
|
9103
|
-
return {
|
|
9149
|
+
return {
|
|
9150
|
+
content: [
|
|
9151
|
+
{
|
|
9152
|
+
type: "text",
|
|
9153
|
+
text: JSON.stringify(await updatePipelineMetadata(project_id, pipeline_id, name)),
|
|
9154
|
+
},
|
|
9155
|
+
],
|
|
9156
|
+
};
|
|
9104
9157
|
}
|
|
9105
9158
|
case "list_deployments": {
|
|
9106
9159
|
const args = ListDeploymentsSchema.parse(params.arguments);
|
|
@@ -9119,23 +9172,58 @@ async function handleToolCall(params) {
|
|
|
9119
9172
|
}
|
|
9120
9173
|
case "create_deployment": {
|
|
9121
9174
|
const { project_id, ...body } = CreateDeploymentSchema.parse(params.arguments);
|
|
9122
|
-
return {
|
|
9175
|
+
return {
|
|
9176
|
+
content: [
|
|
9177
|
+
{
|
|
9178
|
+
type: "text",
|
|
9179
|
+
text: JSON.stringify(await deploymentRequest(project_id, "", "POST", body)),
|
|
9180
|
+
},
|
|
9181
|
+
],
|
|
9182
|
+
};
|
|
9123
9183
|
}
|
|
9124
9184
|
case "update_deployment": {
|
|
9125
9185
|
const { project_id, deployment_id, ...body } = UpdateDeploymentSchema.parse(params.arguments);
|
|
9126
|
-
return {
|
|
9186
|
+
return {
|
|
9187
|
+
content: [
|
|
9188
|
+
{
|
|
9189
|
+
type: "text",
|
|
9190
|
+
text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "PUT", body)),
|
|
9191
|
+
},
|
|
9192
|
+
],
|
|
9193
|
+
};
|
|
9127
9194
|
}
|
|
9128
9195
|
case "delete_deployment": {
|
|
9129
9196
|
const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
|
|
9130
|
-
return {
|
|
9197
|
+
return {
|
|
9198
|
+
content: [
|
|
9199
|
+
{
|
|
9200
|
+
type: "text",
|
|
9201
|
+
text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "DELETE")),
|
|
9202
|
+
},
|
|
9203
|
+
],
|
|
9204
|
+
};
|
|
9131
9205
|
}
|
|
9132
9206
|
case "list_deployment_merge_requests": {
|
|
9133
9207
|
const { project_id, deployment_id, ...query } = ListDeploymentMergeRequestsSchema.parse(params.arguments);
|
|
9134
|
-
return {
|
|
9208
|
+
return {
|
|
9209
|
+
content: [
|
|
9210
|
+
{
|
|
9211
|
+
type: "text",
|
|
9212
|
+
text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/merge_requests`, "GET", undefined, query)),
|
|
9213
|
+
},
|
|
9214
|
+
],
|
|
9215
|
+
};
|
|
9135
9216
|
}
|
|
9136
9217
|
case "approve_deployment": {
|
|
9137
9218
|
const { project_id, deployment_id, ...body } = DeploymentApprovalSchema.parse(params.arguments);
|
|
9138
|
-
return {
|
|
9219
|
+
return {
|
|
9220
|
+
content: [
|
|
9221
|
+
{
|
|
9222
|
+
type: "text",
|
|
9223
|
+
text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/approval`, "POST", body)),
|
|
9224
|
+
},
|
|
9225
|
+
],
|
|
9226
|
+
};
|
|
9139
9227
|
}
|
|
9140
9228
|
case "list_environments": {
|
|
9141
9229
|
const args = ListEnvironmentsSchema.parse(params.arguments);
|
|
@@ -9154,47 +9242,124 @@ async function handleToolCall(params) {
|
|
|
9154
9242
|
}
|
|
9155
9243
|
case "update_environment": {
|
|
9156
9244
|
const { project_id, environment_id, ...body } = UpdateEnvironmentSchema.parse(params.arguments);
|
|
9157
|
-
return {
|
|
9245
|
+
return {
|
|
9246
|
+
content: [
|
|
9247
|
+
{
|
|
9248
|
+
type: "text",
|
|
9249
|
+
text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "PUT", body)),
|
|
9250
|
+
},
|
|
9251
|
+
],
|
|
9252
|
+
};
|
|
9158
9253
|
}
|
|
9159
9254
|
case "delete_environment": {
|
|
9160
9255
|
const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
|
|
9161
|
-
return {
|
|
9256
|
+
return {
|
|
9257
|
+
content: [
|
|
9258
|
+
{
|
|
9259
|
+
type: "text",
|
|
9260
|
+
text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "DELETE")),
|
|
9261
|
+
},
|
|
9262
|
+
],
|
|
9263
|
+
};
|
|
9162
9264
|
}
|
|
9163
9265
|
case "stop_environment": {
|
|
9164
9266
|
const { project_id, environment_id, force } = StopEnvironmentSchema.parse(params.arguments);
|
|
9165
|
-
return {
|
|
9267
|
+
return {
|
|
9268
|
+
content: [
|
|
9269
|
+
{
|
|
9270
|
+
type: "text",
|
|
9271
|
+
text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}/stop`, "POST", undefined, { force })),
|
|
9272
|
+
},
|
|
9273
|
+
],
|
|
9274
|
+
};
|
|
9166
9275
|
}
|
|
9167
9276
|
case "stop_stale_environments": {
|
|
9168
9277
|
const { project_id, before } = StopStaleEnvironmentsSchema.parse(params.arguments);
|
|
9169
|
-
return {
|
|
9278
|
+
return {
|
|
9279
|
+
content: [
|
|
9280
|
+
{
|
|
9281
|
+
type: "text",
|
|
9282
|
+
text: JSON.stringify(await environmentRequest(project_id, "/stop_stale", "POST", undefined, { before })),
|
|
9283
|
+
},
|
|
9284
|
+
],
|
|
9285
|
+
};
|
|
9170
9286
|
}
|
|
9171
9287
|
case "delete_review_app_environments": {
|
|
9172
9288
|
const { project_id, ...query } = DeleteReviewAppEnvironmentsSchema.parse(params.arguments);
|
|
9173
|
-
return {
|
|
9289
|
+
return {
|
|
9290
|
+
content: [
|
|
9291
|
+
{
|
|
9292
|
+
type: "text",
|
|
9293
|
+
text: JSON.stringify(await environmentRequest(project_id, "/review_apps", "DELETE", undefined, query)),
|
|
9294
|
+
},
|
|
9295
|
+
],
|
|
9296
|
+
};
|
|
9174
9297
|
}
|
|
9175
9298
|
case "list_pipeline_triggers": {
|
|
9176
9299
|
const { project_id } = ListPipelineTriggersSchema.parse(params.arguments);
|
|
9177
|
-
return {
|
|
9300
|
+
return {
|
|
9301
|
+
content: [
|
|
9302
|
+
{
|
|
9303
|
+
type: "text",
|
|
9304
|
+
text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id))),
|
|
9305
|
+
},
|
|
9306
|
+
],
|
|
9307
|
+
};
|
|
9178
9308
|
}
|
|
9179
9309
|
case "get_pipeline_trigger": {
|
|
9180
9310
|
const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
|
|
9181
|
-
return {
|
|
9311
|
+
return {
|
|
9312
|
+
content: [
|
|
9313
|
+
{
|
|
9314
|
+
type: "text",
|
|
9315
|
+
text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`))),
|
|
9316
|
+
},
|
|
9317
|
+
],
|
|
9318
|
+
};
|
|
9182
9319
|
}
|
|
9183
9320
|
case "create_pipeline_trigger": {
|
|
9184
9321
|
const { project_id, ...body } = CreatePipelineTriggerSchema.parse(params.arguments);
|
|
9185
|
-
return {
|
|
9322
|
+
return {
|
|
9323
|
+
content: [
|
|
9324
|
+
{
|
|
9325
|
+
type: "text",
|
|
9326
|
+
text: JSON.stringify(await pipelineTriggerRequest(project_id, "", "POST", body)),
|
|
9327
|
+
},
|
|
9328
|
+
],
|
|
9329
|
+
};
|
|
9186
9330
|
}
|
|
9187
9331
|
case "update_pipeline_trigger": {
|
|
9188
9332
|
const { project_id, trigger_id, ...body } = UpdatePipelineTriggerSchema.parse(params.arguments);
|
|
9189
|
-
return {
|
|
9333
|
+
return {
|
|
9334
|
+
content: [
|
|
9335
|
+
{
|
|
9336
|
+
type: "text",
|
|
9337
|
+
text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "PUT", body))),
|
|
9338
|
+
},
|
|
9339
|
+
],
|
|
9340
|
+
};
|
|
9190
9341
|
}
|
|
9191
9342
|
case "delete_pipeline_trigger": {
|
|
9192
9343
|
const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
|
|
9193
|
-
return {
|
|
9344
|
+
return {
|
|
9345
|
+
content: [
|
|
9346
|
+
{
|
|
9347
|
+
type: "text",
|
|
9348
|
+
text: JSON.stringify(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "DELETE")),
|
|
9349
|
+
},
|
|
9350
|
+
],
|
|
9351
|
+
};
|
|
9194
9352
|
}
|
|
9195
9353
|
case "trigger_pipeline": {
|
|
9196
9354
|
const { project_id, token, ref, variables, inputs } = TriggerPipelineSchema.parse(params.arguments);
|
|
9197
|
-
return {
|
|
9355
|
+
return {
|
|
9356
|
+
content: [
|
|
9357
|
+
{
|
|
9358
|
+
type: "text",
|
|
9359
|
+
text: JSON.stringify(await triggerPipeline(project_id, token, ref, variables, inputs)),
|
|
9360
|
+
},
|
|
9361
|
+
],
|
|
9362
|
+
};
|
|
9198
9363
|
}
|
|
9199
9364
|
case "list_pipeline_jobs": {
|
|
9200
9365
|
const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(params.arguments);
|
|
@@ -9561,7 +9726,7 @@ async function handleToolCall(params) {
|
|
|
9561
9726
|
};
|
|
9562
9727
|
}
|
|
9563
9728
|
case "play_pipeline_jobs": {
|
|
9564
|
-
const { project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds } = PlayPipelineJobsSchema.parse(params.arguments);
|
|
9729
|
+
const { project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds, } = PlayPipelineJobsSchema.parse(params.arguments);
|
|
9565
9730
|
const jobs = await playPipelineJobs(project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds);
|
|
9566
9731
|
return { content: [{ type: "text", text: JSON.stringify(jobs) }] };
|
|
9567
9732
|
}
|
|
@@ -9591,7 +9756,11 @@ async function handleToolCall(params) {
|
|
|
9591
9756
|
}
|
|
9592
9757
|
case "erase_pipeline_job": {
|
|
9593
9758
|
const { project_id, job_id } = ErasePipelineJobSchema.parse(params.arguments);
|
|
9594
|
-
return {
|
|
9759
|
+
return {
|
|
9760
|
+
content: [
|
|
9761
|
+
{ type: "text", text: JSON.stringify(await erasePipelineJob(project_id, job_id)) },
|
|
9762
|
+
],
|
|
9763
|
+
};
|
|
9595
9764
|
}
|
|
9596
9765
|
case "wait_for_pipeline": {
|
|
9597
9766
|
const { project_id, pipeline_id, timeout_seconds, poll_interval_seconds } = WaitForPipelineSchema.parse(params.arguments);
|
|
@@ -9623,7 +9792,15 @@ async function handleToolCall(params) {
|
|
|
9623
9792
|
}
|
|
9624
9793
|
const downloadUrl = buildDownloadUrl("job-artifacts", { project_id, job_id });
|
|
9625
9794
|
return {
|
|
9626
|
-
content: [
|
|
9795
|
+
content: [
|
|
9796
|
+
{
|
|
9797
|
+
type: "text",
|
|
9798
|
+
text: JSON.stringify({
|
|
9799
|
+
download_url: downloadUrl,
|
|
9800
|
+
filename: `artifacts_job_${job_id}.zip`,
|
|
9801
|
+
}),
|
|
9802
|
+
},
|
|
9803
|
+
],
|
|
9627
9804
|
};
|
|
9628
9805
|
}
|
|
9629
9806
|
const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
|
|
@@ -10126,7 +10303,12 @@ async function handleToolCall(params) {
|
|
|
10126
10303
|
filename: args.filename,
|
|
10127
10304
|
});
|
|
10128
10305
|
return {
|
|
10129
|
-
content: [
|
|
10306
|
+
content: [
|
|
10307
|
+
{
|
|
10308
|
+
type: "text",
|
|
10309
|
+
text: JSON.stringify({ download_url: downloadUrl, filename: args.filename }),
|
|
10310
|
+
},
|
|
10311
|
+
],
|
|
10130
10312
|
};
|
|
10131
10313
|
}
|
|
10132
10314
|
const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
|
|
@@ -10232,7 +10414,15 @@ async function handleToolCall(params) {
|
|
|
10232
10414
|
direct_asset_path: args.direct_asset_path,
|
|
10233
10415
|
});
|
|
10234
10416
|
return {
|
|
10235
|
-
content: [
|
|
10417
|
+
content: [
|
|
10418
|
+
{
|
|
10419
|
+
type: "text",
|
|
10420
|
+
text: JSON.stringify({
|
|
10421
|
+
download_url: downloadUrl,
|
|
10422
|
+
filename: args.direct_asset_path.split("/").pop() || args.direct_asset_path,
|
|
10423
|
+
}),
|
|
10424
|
+
},
|
|
10425
|
+
],
|
|
10236
10426
|
};
|
|
10237
10427
|
}
|
|
10238
10428
|
const assetContent = await downloadReleaseAsset(args.project_id, args.tag_name, args.direct_asset_path);
|
|
@@ -10423,9 +10613,7 @@ async function handleToolCall(params) {
|
|
|
10423
10613
|
const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
|
|
10424
10614
|
authenticated = jobResponse.ok;
|
|
10425
10615
|
}
|
|
10426
|
-
const versionMetadata = authenticated
|
|
10427
|
-
? await fetchGitLabVersionMetadata()
|
|
10428
|
-
: null;
|
|
10616
|
+
const versionMetadata = authenticated ? await fetchGitLabVersionMetadata() : null;
|
|
10429
10617
|
return {
|
|
10430
10618
|
content: [
|
|
10431
10619
|
{
|
|
@@ -10488,7 +10676,9 @@ async function handleToolCall(params) {
|
|
|
10488
10676
|
});
|
|
10489
10677
|
await handleGitLabError(response);
|
|
10490
10678
|
return {
|
|
10491
|
-
content: [
|
|
10679
|
+
content: [
|
|
10680
|
+
{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }) },
|
|
10681
|
+
],
|
|
10492
10682
|
};
|
|
10493
10683
|
}
|
|
10494
10684
|
case "list_protected_branches": {
|
|
@@ -10563,7 +10753,12 @@ async function handleToolCall(params) {
|
|
|
10563
10753
|
});
|
|
10564
10754
|
await handleGitLabError(response);
|
|
10565
10755
|
return {
|
|
10566
|
-
content: [
|
|
10756
|
+
content: [
|
|
10757
|
+
{
|
|
10758
|
+
type: "text",
|
|
10759
|
+
text: JSON.stringify({ status: "unprotected", branch: args.branch_name }),
|
|
10760
|
+
},
|
|
10761
|
+
],
|
|
10567
10762
|
};
|
|
10568
10763
|
}
|
|
10569
10764
|
case "update_default_branch": {
|
|
@@ -10579,7 +10774,16 @@ async function handleToolCall(params) {
|
|
|
10579
10774
|
await handleGitLabError(response);
|
|
10580
10775
|
const data = await response.json();
|
|
10581
10776
|
return {
|
|
10582
|
-
content: [
|
|
10777
|
+
content: [
|
|
10778
|
+
{
|
|
10779
|
+
type: "text",
|
|
10780
|
+
text: JSON.stringify({
|
|
10781
|
+
status: "updated",
|
|
10782
|
+
default_branch: args.default_branch,
|
|
10783
|
+
project: data,
|
|
10784
|
+
}),
|
|
10785
|
+
},
|
|
10786
|
+
],
|
|
10583
10787
|
};
|
|
10584
10788
|
}
|
|
10585
10789
|
default:
|
|
@@ -10634,6 +10838,7 @@ function buildDownloadProxyDeps() {
|
|
|
10634
10838
|
* Start server with traditional SSE transport
|
|
10635
10839
|
*/
|
|
10636
10840
|
async function startSSEServer() {
|
|
10841
|
+
const { express, rateLimit, ipKeyGenerator, SSEServerTransport } = await loadRemoteHttpStack();
|
|
10637
10842
|
const app = express();
|
|
10638
10843
|
const sseAuthToken = getConfig("sse-auth-token", "SSE_AUTH_TOKEN");
|
|
10639
10844
|
if (MCP_TRUST_PROXY) {
|
|
@@ -10670,7 +10875,8 @@ async function startSSEServer() {
|
|
|
10670
10875
|
const host = req.headers.host;
|
|
10671
10876
|
const forwardedHost = MCP_TRUST_PROXY ? getForwardedRequestHost(req, true) : undefined;
|
|
10672
10877
|
const requestHosts = [host, forwardedHost].filter((value) => Boolean(value));
|
|
10673
|
-
if (requestHosts.length === 0 ||
|
|
10878
|
+
if (requestHosts.length === 0 ||
|
|
10879
|
+
!requestHosts.some(requestHost => effectiveHosts.has(requestHost))) {
|
|
10674
10880
|
res.status(403).json({ error: "Invalid Host header" });
|
|
10675
10881
|
return;
|
|
10676
10882
|
}
|
|
@@ -10845,6 +11051,7 @@ async function startSSEServer() {
|
|
|
10845
11051
|
* Start server with Streamable HTTP transport
|
|
10846
11052
|
*/
|
|
10847
11053
|
async function startStreamableHTTPServer() {
|
|
11054
|
+
const { express, rateLimit, ipKeyGenerator, StreamableHTTPServerTransport, mcpAuthRouter, requireBearerAuth, } = await loadRemoteHttpStack();
|
|
10848
11055
|
const app = express();
|
|
10849
11056
|
// Session IDs are untrusted map keys. A null prototype prevents inherited
|
|
10850
11057
|
// names such as "constructor" from masquerading as live transports.
|
|
@@ -11377,9 +11584,7 @@ async function startStreamableHTTPServer() {
|
|
|
11377
11584
|
// ipKeyGenerator for correct IPv6 subnet handling.
|
|
11378
11585
|
const rateLimitOptions = { keyGenerator: mcpRateLimitKeyGenerator };
|
|
11379
11586
|
const parsedRegisterLimit = Number(process.env.OAUTH_REGISTER_RATE_LIMIT_PER_HOUR);
|
|
11380
|
-
const OAUTH_REGISTER_RATE_LIMIT_PER_HOUR = Number.isInteger(parsedRegisterLimit) && parsedRegisterLimit >= 1
|
|
11381
|
-
? parsedRegisterLimit
|
|
11382
|
-
: 20;
|
|
11587
|
+
const OAUTH_REGISTER_RATE_LIMIT_PER_HOUR = Number.isInteger(parsedRegisterLimit) && parsedRegisterLimit >= 1 ? parsedRegisterLimit : 20;
|
|
11383
11588
|
const clientRegistrationRateLimitOptions = {
|
|
11384
11589
|
...rateLimitOptions,
|
|
11385
11590
|
windowMs: 60 * 60 * 1000, // 1 hour
|
|
@@ -11479,9 +11684,7 @@ async function startStreamableHTTPServer() {
|
|
|
11479
11684
|
? staticMcpBearerAuth
|
|
11480
11685
|
: (_req, _res, next) => next();
|
|
11481
11686
|
const rejectUnknownStatefulSession = (req, res, next) => {
|
|
11482
|
-
const usesStatelessSessions = OAUTH_STATELESS_MODE &&
|
|
11483
|
-
STATELESS_MATERIAL &&
|
|
11484
|
-
(REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH);
|
|
11687
|
+
const usesStatelessSessions = OAUTH_STATELESS_MODE && STATELESS_MATERIAL && (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH);
|
|
11485
11688
|
const sessionId = readMcpSessionIdHeader(req);
|
|
11486
11689
|
if (!usesStatelessSessions && sessionId && !streamableTransports[sessionId]) {
|
|
11487
11690
|
res.status(404).json({ error: "Session not found" });
|
|
@@ -11499,7 +11702,9 @@ async function startStreamableHTTPServer() {
|
|
|
11499
11702
|
// entirely and derive the session auth from either the current request
|
|
11500
11703
|
// headers (init) or a sealed Mcp-Session-Id (subsequent requests).
|
|
11501
11704
|
// Rate limiting is disabled here because there is no shared counter.
|
|
11502
|
-
if (OAUTH_STATELESS_MODE &&
|
|
11705
|
+
if (OAUTH_STATELESS_MODE &&
|
|
11706
|
+
STATELESS_MATERIAL &&
|
|
11707
|
+
(REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
|
|
11503
11708
|
await handleStatelessMcpRequest(req, res, STATELESS_MATERIAL, OAUTH_STATELESS_SESSION_TTL_SECONDS);
|
|
11504
11709
|
return;
|
|
11505
11710
|
}
|
|
@@ -11780,7 +11985,9 @@ async function startStreamableHTTPServer() {
|
|
|
11780
11985
|
});
|
|
11781
11986
|
return;
|
|
11782
11987
|
}
|
|
11783
|
-
if (OAUTH_STATELESS_MODE &&
|
|
11988
|
+
if (OAUTH_STATELESS_MODE &&
|
|
11989
|
+
STATELESS_MATERIAL &&
|
|
11990
|
+
(REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
|
|
11784
11991
|
res.setHeader("Allow", "POST");
|
|
11785
11992
|
res.status(405).json({
|
|
11786
11993
|
error: "Method Not Allowed",
|
|
@@ -11987,6 +12194,22 @@ async function initializeServerByTransportMode(mode) {
|
|
|
11987
12194
|
}
|
|
11988
12195
|
}
|
|
11989
12196
|
}
|
|
12197
|
+
/**
|
|
12198
|
+
* Construct the OAuth client when OAuth is enabled. No network: the token is
|
|
12199
|
+
* acquired lazily on the first tool call via ensureValidOAuthToken, which
|
|
12200
|
+
* avoids blocking startup and opening a browser on transient boot-time errors.
|
|
12201
|
+
* Shared by the MCP server and the human CLI.
|
|
12202
|
+
*/
|
|
12203
|
+
function initOAuthClient() {
|
|
12204
|
+
if (!USE_OAUTH) {
|
|
12205
|
+
return;
|
|
12206
|
+
}
|
|
12207
|
+
if (isCleartextRemoteUrl(GITLAB_API_URL)) {
|
|
12208
|
+
throw new Error(`Refusing to send OAuth tokens over cleartext HTTP to ${GITLAB_API_URL}. ` +
|
|
12209
|
+
"Use an https:// GITLAB_API_URL (http:// is only allowed for localhost).");
|
|
12210
|
+
}
|
|
12211
|
+
oauthClient = createGitLabOAuthClient(GITLAB_API_URL.replace(/\/api\/v4$/, ""));
|
|
12212
|
+
}
|
|
11990
12213
|
/**
|
|
11991
12214
|
* Initialize and run the server
|
|
11992
12215
|
* Main entry point for server startup
|
|
@@ -11999,12 +12222,7 @@ async function runServer() {
|
|
|
11999
12222
|
if (USE_OAUTH) {
|
|
12000
12223
|
logger.info("Using OAuth authentication...");
|
|
12001
12224
|
try {
|
|
12002
|
-
|
|
12003
|
-
// Construct the client synchronously (no network). The token is
|
|
12004
|
-
// acquired lazily on the first tool call via ensureValidOAuthToken,
|
|
12005
|
-
// by which point the network is ready. This avoids blocking startup
|
|
12006
|
-
// and avoids opening a browser on transient boot-time network errors.
|
|
12007
|
-
oauthClient = createGitLabOAuthClient(gitlabBaseUrl);
|
|
12225
|
+
initOAuthClient();
|
|
12008
12226
|
logger.info("OAuth enabled; token acquired lazily on first use.");
|
|
12009
12227
|
}
|
|
12010
12228
|
catch (error) {
|
|
@@ -12026,16 +12244,16 @@ async function runServer() {
|
|
|
12026
12244
|
}
|
|
12027
12245
|
logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.join(", ")}`);
|
|
12028
12246
|
logger.info(`Default GitLab API URL: ${GITLAB_API_URL}`);
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
logger.warn(
|
|
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);
|
|
12039
12257
|
}
|
|
12040
12258
|
if (GITLAB_OAUTH_ALLOWED_GROUPS) {
|
|
12041
12259
|
logger.info(`Group access control enabled for: ${GITLAB_OAUTH_ALLOWED_GROUPS.join(", ")}`);
|
|
@@ -12074,6 +12292,7 @@ async function main() {
|
|
|
12074
12292
|
process.exit(2);
|
|
12075
12293
|
}
|
|
12076
12294
|
try {
|
|
12295
|
+
initOAuthClient();
|
|
12077
12296
|
const result = await handleToolCall({ name: cli.toolName, arguments: cli.args });
|
|
12078
12297
|
const masked = maskCliToolResult(cli.toolName, result);
|
|
12079
12298
|
const formatted = formatToolOutput({
|