@zereight/mcp-gitlab 2.1.70 → 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 +3 -1
- package/README.md +6 -2
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +3 -1
- package/build/config.js +12 -4
- package/build/deprecated-env.js +47 -0
- package/build/index.js +369 -184
- 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/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/credentials.test.js +0 -153
- 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-api-tool-errors.test.js +0 -112
- 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-download-url.test.js +0 -157
- 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 -804
- 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 -484
- 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 -141
- 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");
|
|
@@ -60,17 +61,14 @@ function isConstantTimeSecretMatch(provided, expected) {
|
|
|
60
61
|
}
|
|
61
62
|
import { loadKeyMaterialFromEnv, looksLikeStatelessSessionId, mintSessionId, openSessionId, } from "./stateless/index.js";
|
|
62
63
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
63
|
-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
64
64
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
65
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
66
65
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
67
66
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
68
|
-
import express from "express";
|
|
69
67
|
import fetchCookie from "fetch-cookie";
|
|
70
68
|
import fs from "node:fs";
|
|
71
69
|
import { pipeline as streamPipeline } from "node:stream/promises";
|
|
72
70
|
import os from "node:os";
|
|
73
|
-
import { fetch as undiciFetch, File, FormData } from "undici";
|
|
71
|
+
import { fetch as undiciFetch, File, FormData, } from "undici";
|
|
74
72
|
import path from "node:path";
|
|
75
73
|
import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
76
74
|
import { URL } from "node:url";
|
|
@@ -83,12 +81,11 @@ import { formatCliError, formatToolOutput } from "./cli/output.js";
|
|
|
83
81
|
import { compactMcpToolResult, readOwnRecord } from "./cli/compact-mcp-result.js";
|
|
84
82
|
import { compileDeniedToolsRegex } from "./tools/denied-regex.js";
|
|
85
83
|
import { createGitLabOAuthProvider } from "./oauth-proxy.js";
|
|
86
|
-
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
87
|
-
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
|
88
84
|
import { normalizeProxyClientIpForRateLimit } from "./utils/proxy-client-ip.js";
|
|
89
85
|
import { getForwardedPublicBaseUrl, getForwardedRequestHost, } from "./utils/forwarded-public-base-url.js";
|
|
90
86
|
import { registerDownloadProxy } from "./downloads/proxy.js";
|
|
91
87
|
import { createDownloadToken } from "./utils/download-token.js";
|
|
88
|
+
import { loadRemoteHttpStack } from "./server/remote-http-stack.js";
|
|
92
89
|
import { determineTransportMode, TransportMode } from "./server/transport-mode.js";
|
|
93
90
|
import { formatPrometheusMetrics } from "./server/metrics.js";
|
|
94
91
|
import { SERVER_VERSION } from "./server/version.js";
|
|
@@ -109,9 +106,13 @@ import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./u
|
|
|
109
106
|
import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
|
|
110
107
|
import { applyJmespathToToolResult, getJmespathSyntaxError, JMESPATH_TOOL_ARGUMENT, JMESPATH_TOOL_ARGUMENT_DESCRIPTION, omitJmespathArgument, readJmespathExpression, } from "./utils/jmespath-tool-result.js";
|
|
111
108
|
import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
|
|
112
|
-
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
113
109
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
114
|
-
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";
|
|
115
116
|
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, ErasePipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
|
|
116
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,
|
|
117
118
|
// Discussion Schemas
|
|
@@ -149,7 +150,7 @@ const MCP_PROMPTS = [
|
|
|
149
150
|
render: (args) => `Review merge request !${args.merge_request_iid ?? "<iid>"} in project ${args.project_id ?? "<project_id>"}:\n` +
|
|
150
151
|
`1. Call list_merge_request_changed_files to get the changed file list (use excluded_file_patterns for lockfiles/dist/generated files).\n` +
|
|
151
152
|
`2. Call get_merge_request_file_diff in batches of 3-5 files, prioritizing source over config/tests.\n` +
|
|
152
|
-
`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` +
|
|
153
154
|
`4. Summarize the review verdict in a final top-level note.`,
|
|
154
155
|
},
|
|
155
156
|
{
|
|
@@ -179,6 +180,20 @@ const MCP_PROMPTS = [
|
|
|
179
180
|
`3. Propose a remediation order, calling out any critical/high items with no available fix.`,
|
|
180
181
|
},
|
|
181
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
|
+
}
|
|
182
197
|
/**
|
|
183
198
|
* Create a new MCP Server instance with request handlers registered.
|
|
184
199
|
* Each transport connection gets its own Server instance to prevent
|
|
@@ -186,44 +201,26 @@ const MCP_PROMPTS = [
|
|
|
186
201
|
*/
|
|
187
202
|
/** Builds an MCP server with the tool list filtered by toolsets, permission mode, and policy. */
|
|
188
203
|
function createServer() {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
let filteredTools = GITLAB_DENIED_TOOLS_REGEX
|
|
210
|
-
? toolsAfterReadOnly.filter(tool => !GITLAB_DENIED_TOOLS_REGEX.test(tool.name))
|
|
211
|
-
: [...toolsAfterReadOnly];
|
|
212
|
-
// Step 5.5: Always include discover_tools meta-tool (bypasses toolset filter)
|
|
213
|
-
const discoverTool = allTools.find(t => t.name === "discover_tools");
|
|
214
|
-
const filteredToolNames = new Set(filteredTools.map(t => t.name));
|
|
215
|
-
if (discoverTool && !filteredToolNames.has("discover_tools")) {
|
|
216
|
-
// Respect permission mode and regex denial filters
|
|
217
|
-
const passesPermissionMode = isToolAllowedByPermissionMode("discover_tools");
|
|
218
|
-
const passesRegex = !GITLAB_DENIED_TOOLS_REGEX?.test("discover_tools");
|
|
219
|
-
if (passesPermissionMode && passesRegex) {
|
|
220
|
-
filteredTools.push(discoverTool);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
// Step 5.7: Remove hidden policy tools
|
|
224
|
-
if (hiddenToolSet.size > 0) {
|
|
225
|
-
filteredTools = filteredTools.filter(tool => !hiddenToolSet.has(tool.name));
|
|
226
|
-
}
|
|
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);
|
|
227
224
|
const mcpServer = new McpServer({
|
|
228
225
|
name: SERVER_NAME,
|
|
229
226
|
version: SERVER_VERSION,
|
|
@@ -262,57 +259,8 @@ function createServer() {
|
|
|
262
259
|
};
|
|
263
260
|
});
|
|
264
261
|
mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
265
|
-
// Step 6: Gemini $schema cleanup + annotations (only dynamic step per request)
|
|
266
|
-
// <<< START: Remove $schema for Gemini compatibility >>>
|
|
267
|
-
const tools = filteredTools.map(tool => {
|
|
268
|
-
const modified = { ...tool };
|
|
269
|
-
// Safety net: remove $schema if present (toJSONSchema strips it for zod schemas,
|
|
270
|
-
// but manually-defined schemas like discover_tools may still have it)
|
|
271
|
-
if (modified.inputSchema &&
|
|
272
|
-
typeof modified.inputSchema === "object" &&
|
|
273
|
-
modified.inputSchema !== null) {
|
|
274
|
-
if ("$schema" in modified.inputSchema) {
|
|
275
|
-
modified.inputSchema = { ...modified.inputSchema };
|
|
276
|
-
delete modified.inputSchema.$schema;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
// Add MCP tool annotations
|
|
280
|
-
modified.annotations = {
|
|
281
|
-
...(readOnlyTools.has(tool.name) ? { readOnlyHint: true } : {}),
|
|
282
|
-
...(destructiveTools.has(tool.name) ? { destructiveHint: true } : {}),
|
|
283
|
-
...(approveToolSet.has(tool.name) ? { confirmationHint: true } : {}),
|
|
284
|
-
openWorldHint: true,
|
|
285
|
-
};
|
|
286
|
-
// Inject _confirmed optional parameter for approve-policy tools
|
|
287
|
-
if (approveToolSet.has(tool.name) && modified.inputSchema?.properties) {
|
|
288
|
-
modified.inputSchema = {
|
|
289
|
-
...modified.inputSchema,
|
|
290
|
-
properties: {
|
|
291
|
-
...modified.inputSchema.properties,
|
|
292
|
-
_confirmed: {
|
|
293
|
-
type: "boolean",
|
|
294
|
-
description: "Set to true to confirm execution of this approval-required tool.",
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
if (modified.inputSchema && typeof modified.inputSchema === "object") {
|
|
300
|
-
const properties = modified.inputSchema.properties && typeof modified.inputSchema.properties === "object"
|
|
301
|
-
? { ...modified.inputSchema.properties }
|
|
302
|
-
: {};
|
|
303
|
-
if (!(JMESPATH_TOOL_ARGUMENT in properties)) {
|
|
304
|
-
properties[JMESPATH_TOOL_ARGUMENT] = {
|
|
305
|
-
type: "string",
|
|
306
|
-
description: JMESPATH_TOOL_ARGUMENT_DESCRIPTION,
|
|
307
|
-
};
|
|
308
|
-
modified.inputSchema = { ...modified.inputSchema, properties };
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return modified;
|
|
312
|
-
});
|
|
313
|
-
// <<< END: Remove $schema for Gemini compatibility >>>
|
|
314
262
|
return {
|
|
315
|
-
tools,
|
|
263
|
+
tools: getListedTools(filteredTools, listedToolsCache.revision),
|
|
316
264
|
};
|
|
317
265
|
});
|
|
318
266
|
mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -331,9 +279,7 @@ function createServer() {
|
|
|
331
279
|
(toolName === "download_release_asset" && !IS_REMOTE);
|
|
332
280
|
const filtered = applyJmespathToToolResult(result, jmespathExpression, {
|
|
333
281
|
...completionOptions,
|
|
334
|
-
maskValue: maskingEngine
|
|
335
|
-
? (value) => maskingEngine.maskValue(value)
|
|
336
|
-
: undefined,
|
|
282
|
+
maskValue: maskingEngine ? (value) => maskingEngine.maskValue(value) : undefined,
|
|
337
283
|
});
|
|
338
284
|
const masked = maskingEngine
|
|
339
285
|
? maskingEngine.maskToolResult(filtered, {
|
|
@@ -342,7 +288,7 @@ function createServer() {
|
|
|
342
288
|
: filtered;
|
|
343
289
|
return compactMcpToolResult({
|
|
344
290
|
result: masked,
|
|
345
|
-
enabled: GITLAB_MCP_COMPACT_RESULTS,
|
|
291
|
+
enabled: shouldCompactToolResult(toolName, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_TOOL_NAMES),
|
|
346
292
|
maxChars: GITLAB_MCP_COMPACT_RESULT_CHARS,
|
|
347
293
|
toolName,
|
|
348
294
|
args: readOwnRecord(request.params.arguments),
|
|
@@ -419,25 +365,34 @@ function createServer() {
|
|
|
419
365
|
if (isToolAllowedByPermissionMode(toolName) &&
|
|
420
366
|
!hiddenAndNotDenied &&
|
|
421
367
|
!filteredTools.some(t => t.name === toolName)) {
|
|
422
|
-
throw new Error(
|
|
368
|
+
throw new Error(buildToolUnavailableMessage(toolName));
|
|
423
369
|
}
|
|
424
370
|
// Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
|
|
425
371
|
if (toolName === "discover_tools") {
|
|
426
372
|
const category = request.params.arguments?.category?.trim()?.toLowerCase();
|
|
427
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
|
+
});
|
|
428
380
|
if (!category) {
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
isDefault: def.isDefault,
|
|
435
|
-
}));
|
|
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);
|
|
436
386
|
return logCompletion({
|
|
437
|
-
content: [
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
438
389
|
type: "text",
|
|
439
|
-
text: JSON.stringify({
|
|
440
|
-
|
|
390
|
+
text: JSON.stringify({
|
|
391
|
+
categories,
|
|
392
|
+
hint: "Call discover_tools with a category name to activate it",
|
|
393
|
+
}),
|
|
394
|
+
},
|
|
395
|
+
],
|
|
441
396
|
});
|
|
442
397
|
}
|
|
443
398
|
if (!ALL_TOOLSET_IDS.has(category)) {
|
|
@@ -458,8 +413,8 @@ function createServer() {
|
|
|
458
413
|
isError: true,
|
|
459
414
|
});
|
|
460
415
|
}
|
|
461
|
-
//
|
|
462
|
-
const alreadyActive =
|
|
416
|
+
// Same predicate as the category listing `active` flag.
|
|
417
|
+
const alreadyActive = isToolsetFullyActive(toolsetDef.tools, currentToolNames, isNotActivatable);
|
|
463
418
|
if (alreadyActive) {
|
|
464
419
|
return logCompletion({
|
|
465
420
|
content: [
|
|
@@ -477,11 +432,7 @@ function createServer() {
|
|
|
477
432
|
continue;
|
|
478
433
|
if (currentToolNames.has(tool.name))
|
|
479
434
|
continue;
|
|
480
|
-
if (
|
|
481
|
-
continue;
|
|
482
|
-
if (GITLAB_DENIED_TOOLS_REGEX?.test(tool.name))
|
|
483
|
-
continue;
|
|
484
|
-
if (hiddenToolSet.has(tool.name))
|
|
435
|
+
if (isNotActivatable(tool.name))
|
|
485
436
|
continue;
|
|
486
437
|
newTools.push(tool);
|
|
487
438
|
}
|
|
@@ -496,6 +447,7 @@ function createServer() {
|
|
|
496
447
|
});
|
|
497
448
|
}
|
|
498
449
|
filteredTools.push(...newTools);
|
|
450
|
+
listedToolsCache.revision += 1;
|
|
499
451
|
// Notify client that tool list has changed
|
|
500
452
|
try {
|
|
501
453
|
await mcpServer.server.sendToolListChanged();
|
|
@@ -506,14 +458,16 @@ function createServer() {
|
|
|
506
458
|
const addedNames = newTools.map(t => t.name);
|
|
507
459
|
logger.info({ event: "toolset_activated", category, toolCount: addedNames.length }, `Activated toolset: ${category} (+${addedNames.length} tools)`);
|
|
508
460
|
return logCompletion({
|
|
509
|
-
content: [
|
|
461
|
+
content: [
|
|
462
|
+
{
|
|
510
463
|
type: "text",
|
|
511
464
|
text: JSON.stringify({
|
|
512
465
|
activated: category,
|
|
513
466
|
addedTools: addedNames,
|
|
514
467
|
totalTools: filteredTools.length,
|
|
515
468
|
}),
|
|
516
|
-
}
|
|
469
|
+
},
|
|
470
|
+
],
|
|
517
471
|
});
|
|
518
472
|
}
|
|
519
473
|
// Check approve policy: tool is exposed but requires explicit confirmation
|
|
@@ -570,7 +524,10 @@ function createServer() {
|
|
|
570
524
|
* Validate configuration at startup
|
|
571
525
|
*/
|
|
572
526
|
function isLoopbackBindHost(host) {
|
|
573
|
-
const normalized = host
|
|
527
|
+
const normalized = host
|
|
528
|
+
.trim()
|
|
529
|
+
.toLowerCase()
|
|
530
|
+
.replace(/^\[|\]$/g, "");
|
|
574
531
|
const isIpv4Loopback = /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
575
532
|
return (normalized === "localhost" ||
|
|
576
533
|
isIpv4Loopback ||
|
|
@@ -578,7 +535,10 @@ function isLoopbackBindHost(host) {
|
|
|
578
535
|
normalized === "0:0:0:0:0:0:0:1");
|
|
579
536
|
}
|
|
580
537
|
function formatHostWithPort(host, port) {
|
|
581
|
-
const normalized = host
|
|
538
|
+
const normalized = host
|
|
539
|
+
.trim()
|
|
540
|
+
.toLowerCase()
|
|
541
|
+
.replace(/^\[|\]$/g, "");
|
|
582
542
|
if (!normalized || normalized === "0.0.0.0" || normalized === "::")
|
|
583
543
|
return null;
|
|
584
544
|
if (normalized.includes(":"))
|
|
@@ -929,6 +889,12 @@ const hiddenToolSet = new Set((GITLAB_TOOL_POLICY_HIDDEN_RAW || "")
|
|
|
929
889
|
logger.warn({ event: "unknown_approve_tool", name }, `GITLAB_TOOL_POLICY_APPROVE contains unknown tool: "${name}"`);
|
|
930
890
|
}
|
|
931
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
|
+
}
|
|
932
898
|
for (const name of hiddenToolSet) {
|
|
933
899
|
if (!knownToolNames.has(name)) {
|
|
934
900
|
logger.warn({ event: "unknown_hidden_tool", name }, `GITLAB_TOOL_POLICY_HIDDEN contains unknown tool: "${name}"`);
|
|
@@ -1228,6 +1194,13 @@ const downloadRedirectOptions = () => ({
|
|
|
1228
1194
|
const enabledToolsets = parseEnabledToolsets(GITLAB_TOOLSETS_RAW);
|
|
1229
1195
|
const individuallyEnabledTools = parseIndividualTools(GITLAB_TOOLS_RAW);
|
|
1230
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
|
+
}
|
|
1231
1204
|
// Warn about potentially confusing configuration
|
|
1232
1205
|
if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
|
|
1233
1206
|
logger.warn("GITLAB_TOOLSETS is set alongside legacy flags (USE_PIPELINE, USE_MILESTONE, USE_GITLAB_WIKI). " +
|
|
@@ -1812,7 +1785,9 @@ async function resolveNamesToIds(projectPath, namespaceKind, labelNames, usernam
|
|
|
1812
1785
|
// One alias per label — exact title match via the `title` argument, includes ancestor
|
|
1813
1786
|
// group labels, single round trip with no pagination needed.
|
|
1814
1787
|
const varDefs = labelNames.map((_, i) => `$l${i}: String!`).join(", ");
|
|
1815
|
-
const aliases = labelNames
|
|
1788
|
+
const aliases = labelNames
|
|
1789
|
+
.map((_, i) => `l${i}: labels(title: $l${i}, includeAncestorGroups: true, first: 1) { nodes { id } }`)
|
|
1790
|
+
.join(" ");
|
|
1816
1791
|
const rootField = namespaceKind === "group" ? "group" : "project";
|
|
1817
1792
|
const data = await executeGraphQL(`query($path: ID!, $usernames: [String!]!${varDefs ? `, ${varDefs}` : ""}) {
|
|
1818
1793
|
${rootField}(fullPath: $path) { ${aliases || "__typename"} }
|
|
@@ -3407,7 +3382,9 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
|
|
|
3407
3382
|
async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
|
|
3408
3383
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
3409
3384
|
const basePath = `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
|
|
3410
|
-
const url = new URL(discussionId
|
|
3385
|
+
const url = new URL(discussionId
|
|
3386
|
+
? `${basePath}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`
|
|
3387
|
+
: `${basePath}/notes`);
|
|
3411
3388
|
const payload = { body };
|
|
3412
3389
|
if (createdAt) {
|
|
3413
3390
|
payload.created_at = createdAt;
|
|
@@ -5380,7 +5357,8 @@ async function deploymentRequest(projectId, path = "", method = "GET", body, que
|
|
|
5380
5357
|
url.searchParams.set(key, String(value));
|
|
5381
5358
|
}
|
|
5382
5359
|
const response = await fetch(url.toString(), {
|
|
5383
|
-
...getFetchConfig(),
|
|
5360
|
+
...getFetchConfig(),
|
|
5361
|
+
method,
|
|
5384
5362
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
5385
5363
|
});
|
|
5386
5364
|
await handleGitLabError(response);
|
|
@@ -5430,7 +5408,11 @@ async function environmentRequest(projectId, path, method, body, query) {
|
|
|
5430
5408
|
for (const [key, value] of Object.entries(query ?? {}))
|
|
5431
5409
|
if (value !== undefined)
|
|
5432
5410
|
url.searchParams.set(key, String(value));
|
|
5433
|
-
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
|
+
});
|
|
5434
5416
|
await handleGitLabError(response);
|
|
5435
5417
|
return response.status === 204 ? { success: true } : response.json();
|
|
5436
5418
|
}
|
|
@@ -5450,7 +5432,11 @@ async function triggerPipeline(projectId, token, ref, variables, inputs) {
|
|
|
5450
5432
|
body.variables = variables;
|
|
5451
5433
|
if (inputs)
|
|
5452
5434
|
body.inputs = inputs;
|
|
5453
|
-
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
|
+
});
|
|
5454
5440
|
await handleGitLabError(response);
|
|
5455
5441
|
return response.json();
|
|
5456
5442
|
}
|
|
@@ -8021,8 +8007,7 @@ async function handleToolCall(params) {
|
|
|
8021
8007
|
}
|
|
8022
8008
|
case "push_files": {
|
|
8023
8009
|
const args = PushFilesSchema.parse(params.arguments);
|
|
8024
|
-
if (GITLAB_PERMISSION_MODE === "modify" &&
|
|
8025
|
-
fileOperationsIncludeDeleteOrMove(args.files)) {
|
|
8010
|
+
if (GITLAB_PERMISSION_MODE === "modify" && fileOperationsIncludeDeleteOrMove(args.files)) {
|
|
8026
8011
|
throw new Error("push_files does not allow delete or move actions in modify mode");
|
|
8027
8012
|
}
|
|
8028
8013
|
const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(f => ({
|
|
@@ -8893,7 +8878,14 @@ async function handleToolCall(params) {
|
|
|
8893
8878
|
const args = DeleteWorkItemEmojiReactionSchema.parse(params.arguments);
|
|
8894
8879
|
const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
|
|
8895
8880
|
const result = await removeGraphQLAwardEmoji(workItemGID, args.name);
|
|
8896
|
-
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
|
+
};
|
|
8897
8889
|
}
|
|
8898
8890
|
case "create_work_item_note_emoji_reaction": {
|
|
8899
8891
|
const args = CreateWorkItemNoteEmojiReactionSchema.parse(params.arguments);
|
|
@@ -8903,7 +8895,14 @@ async function handleToolCall(params) {
|
|
|
8903
8895
|
case "delete_work_item_note_emoji_reaction": {
|
|
8904
8896
|
const args = DeleteWorkItemNoteEmojiReactionSchema.parse(params.arguments);
|
|
8905
8897
|
const result = await removeGraphQLAwardEmoji(args.note_id, args.name);
|
|
8906
|
-
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
|
+
};
|
|
8907
8906
|
}
|
|
8908
8907
|
case "get_timeline_events": {
|
|
8909
8908
|
const args = GetTimelineEventsSchema.parse(params.arguments);
|
|
@@ -9106,23 +9105,55 @@ async function handleToolCall(params) {
|
|
|
9106
9105
|
}
|
|
9107
9106
|
case "get_pipeline_variables": {
|
|
9108
9107
|
const { project_id, pipeline_id, page, per_page } = GetPipelineVariablesSchema.parse(params.arguments);
|
|
9109
|
-
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
|
+
};
|
|
9110
9116
|
}
|
|
9111
9117
|
case "get_pipeline_test_report": {
|
|
9112
9118
|
const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
|
|
9113
|
-
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
|
+
};
|
|
9114
9127
|
}
|
|
9115
9128
|
case "get_pipeline_test_report_summary": {
|
|
9116
9129
|
const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
|
|
9117
|
-
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
|
+
};
|
|
9118
9138
|
}
|
|
9119
9139
|
case "delete_pipeline": {
|
|
9120
9140
|
const { project_id, pipeline_id } = DeletePipelineSchema.parse(params.arguments);
|
|
9121
|
-
return {
|
|
9141
|
+
return {
|
|
9142
|
+
content: [
|
|
9143
|
+
{ type: "text", text: JSON.stringify(await deletePipeline(project_id, pipeline_id)) },
|
|
9144
|
+
],
|
|
9145
|
+
};
|
|
9122
9146
|
}
|
|
9123
9147
|
case "update_pipeline_metadata": {
|
|
9124
9148
|
const { project_id, pipeline_id, name } = UpdatePipelineMetadataSchema.parse(params.arguments);
|
|
9125
|
-
return {
|
|
9149
|
+
return {
|
|
9150
|
+
content: [
|
|
9151
|
+
{
|
|
9152
|
+
type: "text",
|
|
9153
|
+
text: JSON.stringify(await updatePipelineMetadata(project_id, pipeline_id, name)),
|
|
9154
|
+
},
|
|
9155
|
+
],
|
|
9156
|
+
};
|
|
9126
9157
|
}
|
|
9127
9158
|
case "list_deployments": {
|
|
9128
9159
|
const args = ListDeploymentsSchema.parse(params.arguments);
|
|
@@ -9141,23 +9172,58 @@ async function handleToolCall(params) {
|
|
|
9141
9172
|
}
|
|
9142
9173
|
case "create_deployment": {
|
|
9143
9174
|
const { project_id, ...body } = CreateDeploymentSchema.parse(params.arguments);
|
|
9144
|
-
return {
|
|
9175
|
+
return {
|
|
9176
|
+
content: [
|
|
9177
|
+
{
|
|
9178
|
+
type: "text",
|
|
9179
|
+
text: JSON.stringify(await deploymentRequest(project_id, "", "POST", body)),
|
|
9180
|
+
},
|
|
9181
|
+
],
|
|
9182
|
+
};
|
|
9145
9183
|
}
|
|
9146
9184
|
case "update_deployment": {
|
|
9147
9185
|
const { project_id, deployment_id, ...body } = UpdateDeploymentSchema.parse(params.arguments);
|
|
9148
|
-
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
|
+
};
|
|
9149
9194
|
}
|
|
9150
9195
|
case "delete_deployment": {
|
|
9151
9196
|
const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
|
|
9152
|
-
return {
|
|
9197
|
+
return {
|
|
9198
|
+
content: [
|
|
9199
|
+
{
|
|
9200
|
+
type: "text",
|
|
9201
|
+
text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "DELETE")),
|
|
9202
|
+
},
|
|
9203
|
+
],
|
|
9204
|
+
};
|
|
9153
9205
|
}
|
|
9154
9206
|
case "list_deployment_merge_requests": {
|
|
9155
9207
|
const { project_id, deployment_id, ...query } = ListDeploymentMergeRequestsSchema.parse(params.arguments);
|
|
9156
|
-
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
|
+
};
|
|
9157
9216
|
}
|
|
9158
9217
|
case "approve_deployment": {
|
|
9159
9218
|
const { project_id, deployment_id, ...body } = DeploymentApprovalSchema.parse(params.arguments);
|
|
9160
|
-
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
|
+
};
|
|
9161
9227
|
}
|
|
9162
9228
|
case "list_environments": {
|
|
9163
9229
|
const args = ListEnvironmentsSchema.parse(params.arguments);
|
|
@@ -9176,47 +9242,124 @@ async function handleToolCall(params) {
|
|
|
9176
9242
|
}
|
|
9177
9243
|
case "update_environment": {
|
|
9178
9244
|
const { project_id, environment_id, ...body } = UpdateEnvironmentSchema.parse(params.arguments);
|
|
9179
|
-
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
|
+
};
|
|
9180
9253
|
}
|
|
9181
9254
|
case "delete_environment": {
|
|
9182
9255
|
const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
|
|
9183
|
-
return {
|
|
9256
|
+
return {
|
|
9257
|
+
content: [
|
|
9258
|
+
{
|
|
9259
|
+
type: "text",
|
|
9260
|
+
text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "DELETE")),
|
|
9261
|
+
},
|
|
9262
|
+
],
|
|
9263
|
+
};
|
|
9184
9264
|
}
|
|
9185
9265
|
case "stop_environment": {
|
|
9186
9266
|
const { project_id, environment_id, force } = StopEnvironmentSchema.parse(params.arguments);
|
|
9187
|
-
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
|
+
};
|
|
9188
9275
|
}
|
|
9189
9276
|
case "stop_stale_environments": {
|
|
9190
9277
|
const { project_id, before } = StopStaleEnvironmentsSchema.parse(params.arguments);
|
|
9191
|
-
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
|
+
};
|
|
9192
9286
|
}
|
|
9193
9287
|
case "delete_review_app_environments": {
|
|
9194
9288
|
const { project_id, ...query } = DeleteReviewAppEnvironmentsSchema.parse(params.arguments);
|
|
9195
|
-
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
|
+
};
|
|
9196
9297
|
}
|
|
9197
9298
|
case "list_pipeline_triggers": {
|
|
9198
9299
|
const { project_id } = ListPipelineTriggersSchema.parse(params.arguments);
|
|
9199
|
-
return {
|
|
9300
|
+
return {
|
|
9301
|
+
content: [
|
|
9302
|
+
{
|
|
9303
|
+
type: "text",
|
|
9304
|
+
text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id))),
|
|
9305
|
+
},
|
|
9306
|
+
],
|
|
9307
|
+
};
|
|
9200
9308
|
}
|
|
9201
9309
|
case "get_pipeline_trigger": {
|
|
9202
9310
|
const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
|
|
9203
|
-
return {
|
|
9311
|
+
return {
|
|
9312
|
+
content: [
|
|
9313
|
+
{
|
|
9314
|
+
type: "text",
|
|
9315
|
+
text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`))),
|
|
9316
|
+
},
|
|
9317
|
+
],
|
|
9318
|
+
};
|
|
9204
9319
|
}
|
|
9205
9320
|
case "create_pipeline_trigger": {
|
|
9206
9321
|
const { project_id, ...body } = CreatePipelineTriggerSchema.parse(params.arguments);
|
|
9207
|
-
return {
|
|
9322
|
+
return {
|
|
9323
|
+
content: [
|
|
9324
|
+
{
|
|
9325
|
+
type: "text",
|
|
9326
|
+
text: JSON.stringify(await pipelineTriggerRequest(project_id, "", "POST", body)),
|
|
9327
|
+
},
|
|
9328
|
+
],
|
|
9329
|
+
};
|
|
9208
9330
|
}
|
|
9209
9331
|
case "update_pipeline_trigger": {
|
|
9210
9332
|
const { project_id, trigger_id, ...body } = UpdatePipelineTriggerSchema.parse(params.arguments);
|
|
9211
|
-
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
|
+
};
|
|
9212
9341
|
}
|
|
9213
9342
|
case "delete_pipeline_trigger": {
|
|
9214
9343
|
const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
|
|
9215
|
-
return {
|
|
9344
|
+
return {
|
|
9345
|
+
content: [
|
|
9346
|
+
{
|
|
9347
|
+
type: "text",
|
|
9348
|
+
text: JSON.stringify(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "DELETE")),
|
|
9349
|
+
},
|
|
9350
|
+
],
|
|
9351
|
+
};
|
|
9216
9352
|
}
|
|
9217
9353
|
case "trigger_pipeline": {
|
|
9218
9354
|
const { project_id, token, ref, variables, inputs } = TriggerPipelineSchema.parse(params.arguments);
|
|
9219
|
-
return {
|
|
9355
|
+
return {
|
|
9356
|
+
content: [
|
|
9357
|
+
{
|
|
9358
|
+
type: "text",
|
|
9359
|
+
text: JSON.stringify(await triggerPipeline(project_id, token, ref, variables, inputs)),
|
|
9360
|
+
},
|
|
9361
|
+
],
|
|
9362
|
+
};
|
|
9220
9363
|
}
|
|
9221
9364
|
case "list_pipeline_jobs": {
|
|
9222
9365
|
const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(params.arguments);
|
|
@@ -9583,7 +9726,7 @@ async function handleToolCall(params) {
|
|
|
9583
9726
|
};
|
|
9584
9727
|
}
|
|
9585
9728
|
case "play_pipeline_jobs": {
|
|
9586
|
-
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);
|
|
9587
9730
|
const jobs = await playPipelineJobs(project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds);
|
|
9588
9731
|
return { content: [{ type: "text", text: JSON.stringify(jobs) }] };
|
|
9589
9732
|
}
|
|
@@ -9613,7 +9756,11 @@ async function handleToolCall(params) {
|
|
|
9613
9756
|
}
|
|
9614
9757
|
case "erase_pipeline_job": {
|
|
9615
9758
|
const { project_id, job_id } = ErasePipelineJobSchema.parse(params.arguments);
|
|
9616
|
-
return {
|
|
9759
|
+
return {
|
|
9760
|
+
content: [
|
|
9761
|
+
{ type: "text", text: JSON.stringify(await erasePipelineJob(project_id, job_id)) },
|
|
9762
|
+
],
|
|
9763
|
+
};
|
|
9617
9764
|
}
|
|
9618
9765
|
case "wait_for_pipeline": {
|
|
9619
9766
|
const { project_id, pipeline_id, timeout_seconds, poll_interval_seconds } = WaitForPipelineSchema.parse(params.arguments);
|
|
@@ -9645,7 +9792,15 @@ async function handleToolCall(params) {
|
|
|
9645
9792
|
}
|
|
9646
9793
|
const downloadUrl = buildDownloadUrl("job-artifacts", { project_id, job_id });
|
|
9647
9794
|
return {
|
|
9648
|
-
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
|
+
],
|
|
9649
9804
|
};
|
|
9650
9805
|
}
|
|
9651
9806
|
const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
|
|
@@ -10148,7 +10303,12 @@ async function handleToolCall(params) {
|
|
|
10148
10303
|
filename: args.filename,
|
|
10149
10304
|
});
|
|
10150
10305
|
return {
|
|
10151
|
-
content: [
|
|
10306
|
+
content: [
|
|
10307
|
+
{
|
|
10308
|
+
type: "text",
|
|
10309
|
+
text: JSON.stringify({ download_url: downloadUrl, filename: args.filename }),
|
|
10310
|
+
},
|
|
10311
|
+
],
|
|
10152
10312
|
};
|
|
10153
10313
|
}
|
|
10154
10314
|
const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
|
|
@@ -10254,7 +10414,15 @@ async function handleToolCall(params) {
|
|
|
10254
10414
|
direct_asset_path: args.direct_asset_path,
|
|
10255
10415
|
});
|
|
10256
10416
|
return {
|
|
10257
|
-
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
|
+
],
|
|
10258
10426
|
};
|
|
10259
10427
|
}
|
|
10260
10428
|
const assetContent = await downloadReleaseAsset(args.project_id, args.tag_name, args.direct_asset_path);
|
|
@@ -10445,9 +10613,7 @@ async function handleToolCall(params) {
|
|
|
10445
10613
|
const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
|
|
10446
10614
|
authenticated = jobResponse.ok;
|
|
10447
10615
|
}
|
|
10448
|
-
const versionMetadata = authenticated
|
|
10449
|
-
? await fetchGitLabVersionMetadata()
|
|
10450
|
-
: null;
|
|
10616
|
+
const versionMetadata = authenticated ? await fetchGitLabVersionMetadata() : null;
|
|
10451
10617
|
return {
|
|
10452
10618
|
content: [
|
|
10453
10619
|
{
|
|
@@ -10510,7 +10676,9 @@ async function handleToolCall(params) {
|
|
|
10510
10676
|
});
|
|
10511
10677
|
await handleGitLabError(response);
|
|
10512
10678
|
return {
|
|
10513
|
-
content: [
|
|
10679
|
+
content: [
|
|
10680
|
+
{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }) },
|
|
10681
|
+
],
|
|
10514
10682
|
};
|
|
10515
10683
|
}
|
|
10516
10684
|
case "list_protected_branches": {
|
|
@@ -10585,7 +10753,12 @@ async function handleToolCall(params) {
|
|
|
10585
10753
|
});
|
|
10586
10754
|
await handleGitLabError(response);
|
|
10587
10755
|
return {
|
|
10588
|
-
content: [
|
|
10756
|
+
content: [
|
|
10757
|
+
{
|
|
10758
|
+
type: "text",
|
|
10759
|
+
text: JSON.stringify({ status: "unprotected", branch: args.branch_name }),
|
|
10760
|
+
},
|
|
10761
|
+
],
|
|
10589
10762
|
};
|
|
10590
10763
|
}
|
|
10591
10764
|
case "update_default_branch": {
|
|
@@ -10601,7 +10774,16 @@ async function handleToolCall(params) {
|
|
|
10601
10774
|
await handleGitLabError(response);
|
|
10602
10775
|
const data = await response.json();
|
|
10603
10776
|
return {
|
|
10604
|
-
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
|
+
],
|
|
10605
10787
|
};
|
|
10606
10788
|
}
|
|
10607
10789
|
default:
|
|
@@ -10656,6 +10838,7 @@ function buildDownloadProxyDeps() {
|
|
|
10656
10838
|
* Start server with traditional SSE transport
|
|
10657
10839
|
*/
|
|
10658
10840
|
async function startSSEServer() {
|
|
10841
|
+
const { express, rateLimit, ipKeyGenerator, SSEServerTransport } = await loadRemoteHttpStack();
|
|
10659
10842
|
const app = express();
|
|
10660
10843
|
const sseAuthToken = getConfig("sse-auth-token", "SSE_AUTH_TOKEN");
|
|
10661
10844
|
if (MCP_TRUST_PROXY) {
|
|
@@ -10692,7 +10875,8 @@ async function startSSEServer() {
|
|
|
10692
10875
|
const host = req.headers.host;
|
|
10693
10876
|
const forwardedHost = MCP_TRUST_PROXY ? getForwardedRequestHost(req, true) : undefined;
|
|
10694
10877
|
const requestHosts = [host, forwardedHost].filter((value) => Boolean(value));
|
|
10695
|
-
if (requestHosts.length === 0 ||
|
|
10878
|
+
if (requestHosts.length === 0 ||
|
|
10879
|
+
!requestHosts.some(requestHost => effectiveHosts.has(requestHost))) {
|
|
10696
10880
|
res.status(403).json({ error: "Invalid Host header" });
|
|
10697
10881
|
return;
|
|
10698
10882
|
}
|
|
@@ -10867,6 +11051,7 @@ async function startSSEServer() {
|
|
|
10867
11051
|
* Start server with Streamable HTTP transport
|
|
10868
11052
|
*/
|
|
10869
11053
|
async function startStreamableHTTPServer() {
|
|
11054
|
+
const { express, rateLimit, ipKeyGenerator, StreamableHTTPServerTransport, mcpAuthRouter, requireBearerAuth, } = await loadRemoteHttpStack();
|
|
10870
11055
|
const app = express();
|
|
10871
11056
|
// Session IDs are untrusted map keys. A null prototype prevents inherited
|
|
10872
11057
|
// names such as "constructor" from masquerading as live transports.
|
|
@@ -11399,9 +11584,7 @@ async function startStreamableHTTPServer() {
|
|
|
11399
11584
|
// ipKeyGenerator for correct IPv6 subnet handling.
|
|
11400
11585
|
const rateLimitOptions = { keyGenerator: mcpRateLimitKeyGenerator };
|
|
11401
11586
|
const parsedRegisterLimit = Number(process.env.OAUTH_REGISTER_RATE_LIMIT_PER_HOUR);
|
|
11402
|
-
const OAUTH_REGISTER_RATE_LIMIT_PER_HOUR = Number.isInteger(parsedRegisterLimit) && parsedRegisterLimit >= 1
|
|
11403
|
-
? parsedRegisterLimit
|
|
11404
|
-
: 20;
|
|
11587
|
+
const OAUTH_REGISTER_RATE_LIMIT_PER_HOUR = Number.isInteger(parsedRegisterLimit) && parsedRegisterLimit >= 1 ? parsedRegisterLimit : 20;
|
|
11405
11588
|
const clientRegistrationRateLimitOptions = {
|
|
11406
11589
|
...rateLimitOptions,
|
|
11407
11590
|
windowMs: 60 * 60 * 1000, // 1 hour
|
|
@@ -11501,9 +11684,7 @@ async function startStreamableHTTPServer() {
|
|
|
11501
11684
|
? staticMcpBearerAuth
|
|
11502
11685
|
: (_req, _res, next) => next();
|
|
11503
11686
|
const rejectUnknownStatefulSession = (req, res, next) => {
|
|
11504
|
-
const usesStatelessSessions = OAUTH_STATELESS_MODE &&
|
|
11505
|
-
STATELESS_MATERIAL &&
|
|
11506
|
-
(REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH);
|
|
11687
|
+
const usesStatelessSessions = OAUTH_STATELESS_MODE && STATELESS_MATERIAL && (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH);
|
|
11507
11688
|
const sessionId = readMcpSessionIdHeader(req);
|
|
11508
11689
|
if (!usesStatelessSessions && sessionId && !streamableTransports[sessionId]) {
|
|
11509
11690
|
res.status(404).json({ error: "Session not found" });
|
|
@@ -11521,7 +11702,9 @@ async function startStreamableHTTPServer() {
|
|
|
11521
11702
|
// entirely and derive the session auth from either the current request
|
|
11522
11703
|
// headers (init) or a sealed Mcp-Session-Id (subsequent requests).
|
|
11523
11704
|
// Rate limiting is disabled here because there is no shared counter.
|
|
11524
|
-
if (OAUTH_STATELESS_MODE &&
|
|
11705
|
+
if (OAUTH_STATELESS_MODE &&
|
|
11706
|
+
STATELESS_MATERIAL &&
|
|
11707
|
+
(REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
|
|
11525
11708
|
await handleStatelessMcpRequest(req, res, STATELESS_MATERIAL, OAUTH_STATELESS_SESSION_TTL_SECONDS);
|
|
11526
11709
|
return;
|
|
11527
11710
|
}
|
|
@@ -11802,7 +11985,9 @@ async function startStreamableHTTPServer() {
|
|
|
11802
11985
|
});
|
|
11803
11986
|
return;
|
|
11804
11987
|
}
|
|
11805
|
-
if (OAUTH_STATELESS_MODE &&
|
|
11988
|
+
if (OAUTH_STATELESS_MODE &&
|
|
11989
|
+
STATELESS_MATERIAL &&
|
|
11990
|
+
(REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
|
|
11806
11991
|
res.setHeader("Allow", "POST");
|
|
11807
11992
|
res.status(405).json({
|
|
11808
11993
|
error: "Method Not Allowed",
|
|
@@ -12059,16 +12244,16 @@ async function runServer() {
|
|
|
12059
12244
|
}
|
|
12060
12245
|
logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.join(", ")}`);
|
|
12061
12246
|
logger.info(`Default GitLab API URL: ${GITLAB_API_URL}`);
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
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);
|
|
12072
12257
|
}
|
|
12073
12258
|
if (GITLAB_OAUTH_ALLOWED_GROUPS) {
|
|
12074
12259
|
logger.info(`Group access control enabled for: ${GITLAB_OAUTH_ALLOWED_GROUPS.join(", ")}`);
|