@zereight/mcp-gitlab 2.1.70 → 2.2.1
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 +9 -5
- package/README.md +15 -11
- package/README.pt-BR.md +9 -5
- package/README.zh-CN.md +9 -5
- package/build/config.js +12 -4
- package/build/deprecated-env.js +63 -0
- package/build/index.js +430 -200
- 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 +232 -63
- package/build/tools/tool-descriptions.js +3 -3
- 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
|
|
@@ -548,9 +502,9 @@ function createServer() {
|
|
|
548
502
|
return logCompletion(result);
|
|
549
503
|
}
|
|
550
504
|
catch (error) {
|
|
551
|
-
const
|
|
505
|
+
const isRecoverableToolError = error instanceof GitLabApiError || error instanceof ToolInputError;
|
|
552
506
|
const safeError = logError(error);
|
|
553
|
-
if (
|
|
507
|
+
if (isRecoverableToolError) {
|
|
554
508
|
return {
|
|
555
509
|
content: [
|
|
556
510
|
{
|
|
@@ -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). " +
|
|
@@ -1347,6 +1320,13 @@ class GitLabApiError extends Error {
|
|
|
1347
1320
|
this.name = "GitLabApiError";
|
|
1348
1321
|
}
|
|
1349
1322
|
}
|
|
1323
|
+
class ToolInputError extends Error {
|
|
1324
|
+
constructor(message) {
|
|
1325
|
+
super(message);
|
|
1326
|
+
this.name = "ToolInputError";
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const MERGE_REQUEST_IDENTIFIER_REQUIRED = "Either merge_request_iid or source_branch must be provided";
|
|
1350
1330
|
/**
|
|
1351
1331
|
* Utility function for handling GitLab API errors
|
|
1352
1332
|
* API 에러 처리를 위한 유틸리티 함수 (Utility function for handling API errors)
|
|
@@ -1373,6 +1353,39 @@ async function handleGitLabError(response) {
|
|
|
1373
1353
|
}
|
|
1374
1354
|
}
|
|
1375
1355
|
}
|
|
1356
|
+
const USER_API_INSUFFICIENT_SCOPE_MESSAGE = "GitLab API error: 403 Forbidden on GET /api/v4/user (insufficient scope). The token was accepted, but read_user (classic PAT) or User API read (fine-grained User:Read) is required for whoami, my_issues, and other current-user tools.";
|
|
1357
|
+
const USER_API_HEALTH_WARNING = "GET /api/v4/user returned 403 (insufficient scope). The token was accepted and other tools may still work. Grant read_user (classic) or User:Read (fine-grained) for whoami, my_issues, and version metadata.";
|
|
1358
|
+
function isGitLabUserApiRateLimitBody(errorBody) {
|
|
1359
|
+
return errorBody.includes("User API Key Rate limit exceeded");
|
|
1360
|
+
}
|
|
1361
|
+
function isGitLabInsufficientUserScopeBody(errorBody) {
|
|
1362
|
+
// Fine-grained PATs use insufficient_granular_scope, which does not contain insufficient_scope.
|
|
1363
|
+
return (errorBody.includes("insufficient_granular_scope") || errorBody.includes("insufficient_scope"));
|
|
1364
|
+
}
|
|
1365
|
+
async function isInsufficientUserScopeForbiddenAsync(response) {
|
|
1366
|
+
if (response.status !== 403) {
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
const errorBody = await response.text();
|
|
1370
|
+
return isGitLabInsufficientUserScopeBody(errorBody);
|
|
1371
|
+
}
|
|
1372
|
+
async function throwUserApiScopeErrorUnlessRateLimitedAsync(response) {
|
|
1373
|
+
const errorBody = await response.clone().text();
|
|
1374
|
+
// Rate-limit and other 403s keep the upstream body via handleGitLabError.
|
|
1375
|
+
if (isGitLabUserApiRateLimitBody(errorBody) || !isGitLabInsufficientUserScopeBody(errorBody)) {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
throw new GitLabApiError(USER_API_INSUFFICIENT_SCOPE_MESSAGE);
|
|
1379
|
+
}
|
|
1380
|
+
async function authenticateWithJobTokenFallbackAsync(response) {
|
|
1381
|
+
if (response.ok ||
|
|
1382
|
+
(response.status !== 401 && response.status !== 403) ||
|
|
1383
|
+
!(GITLAB_JOB_TOKEN || usesJobTokenHeader())) {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
const jobResponse = await fetch(`${getEffectiveApiUrl()}/job`, getFetchConfig());
|
|
1387
|
+
return jobResponse.ok;
|
|
1388
|
+
}
|
|
1376
1389
|
/**
|
|
1377
1390
|
* @param {string} projectId - The project ID parameter passed to the function
|
|
1378
1391
|
* @returns {string} The project ID to use for the API call
|
|
@@ -1812,7 +1825,9 @@ async function resolveNamesToIds(projectPath, namespaceKind, labelNames, usernam
|
|
|
1812
1825
|
// One alias per label — exact title match via the `title` argument, includes ancestor
|
|
1813
1826
|
// group labels, single round trip with no pagination needed.
|
|
1814
1827
|
const varDefs = labelNames.map((_, i) => `$l${i}: String!`).join(", ");
|
|
1815
|
-
const aliases = labelNames
|
|
1828
|
+
const aliases = labelNames
|
|
1829
|
+
.map((_, i) => `l${i}: labels(title: $l${i}, includeAncestorGroups: true, first: 1) { nodes { id } }`)
|
|
1830
|
+
.join(" ");
|
|
1816
1831
|
const rootField = namespaceKind === "group" ? "group" : "project";
|
|
1817
1832
|
const data = await executeGraphQL(`query($path: ID!, $usernames: [String!]!${varDefs ? `, ${varDefs}` : ""}) {
|
|
1818
1833
|
${rootField}(fullPath: $path) { ${aliases || "__typename"} }
|
|
@@ -3407,7 +3422,9 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
|
|
|
3407
3422
|
async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
|
|
3408
3423
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
3409
3424
|
const basePath = `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
|
|
3410
|
-
const url = new URL(discussionId
|
|
3425
|
+
const url = new URL(discussionId
|
|
3426
|
+
? `${basePath}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`
|
|
3427
|
+
: `${basePath}/notes`);
|
|
3411
3428
|
const payload = { body };
|
|
3412
3429
|
if (createdAt) {
|
|
3413
3430
|
payload.created_at = createdAt;
|
|
@@ -3816,7 +3833,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
|
|
|
3816
3833
|
url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
|
|
3817
3834
|
}
|
|
3818
3835
|
else {
|
|
3819
|
-
throw new
|
|
3836
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
3820
3837
|
}
|
|
3821
3838
|
const response = await fetch(url.toString(), {
|
|
3822
3839
|
...getFetchConfig(),
|
|
@@ -4050,7 +4067,7 @@ async function buildMergeRequestDeploymentSummary(projectId, mergeRequest) {
|
|
|
4050
4067
|
async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view) {
|
|
4051
4068
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4052
4069
|
if (!mergeRequestIid && !branchName) {
|
|
4053
|
-
throw new
|
|
4070
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4054
4071
|
}
|
|
4055
4072
|
if (branchName && !mergeRequestIid) {
|
|
4056
4073
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -4080,7 +4097,7 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view
|
|
|
4080
4097
|
async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, page, perPage, unidiff) {
|
|
4081
4098
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4082
4099
|
if (!mergeRequestIid && !branchName) {
|
|
4083
|
-
throw new
|
|
4100
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4084
4101
|
}
|
|
4085
4102
|
if (branchName && !mergeRequestIid) {
|
|
4086
4103
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -4116,7 +4133,7 @@ async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, pag
|
|
|
4116
4133
|
async function listMergeRequestChangedFiles(projectId, mergeRequestIid, branchName, excludedFilePatterns) {
|
|
4117
4134
|
projectId = decodeURIComponent(projectId);
|
|
4118
4135
|
if (!mergeRequestIid && !branchName) {
|
|
4119
|
-
throw new
|
|
4136
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4120
4137
|
}
|
|
4121
4138
|
if (branchName && !mergeRequestIid) {
|
|
4122
4139
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -4150,7 +4167,7 @@ async function listMergeRequestChangedFiles(projectId, mergeRequestIid, branchNa
|
|
|
4150
4167
|
async function getMergeRequestFileDiff(projectId, filePaths, mergeRequestIid, branchName, unidiff) {
|
|
4151
4168
|
projectId = decodeURIComponent(projectId);
|
|
4152
4169
|
if (!mergeRequestIid && !branchName) {
|
|
4153
|
-
throw new
|
|
4170
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4154
4171
|
}
|
|
4155
4172
|
if (branchName && !mergeRequestIid) {
|
|
4156
4173
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -4235,7 +4252,7 @@ async function getBranchDiffs(projectId, from, to, straight) {
|
|
|
4235
4252
|
async function updateMergeRequest(projectId, options, mergeRequestIid, branchName) {
|
|
4236
4253
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4237
4254
|
if (!mergeRequestIid && !branchName) {
|
|
4238
|
-
throw new
|
|
4255
|
+
throw new ToolInputError(MERGE_REQUEST_IDENTIFIER_REQUIRED);
|
|
4239
4256
|
}
|
|
4240
4257
|
if (branchName && !mergeRequestIid) {
|
|
4241
4258
|
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
@@ -5380,7 +5397,8 @@ async function deploymentRequest(projectId, path = "", method = "GET", body, que
|
|
|
5380
5397
|
url.searchParams.set(key, String(value));
|
|
5381
5398
|
}
|
|
5382
5399
|
const response = await fetch(url.toString(), {
|
|
5383
|
-
...getFetchConfig(),
|
|
5400
|
+
...getFetchConfig(),
|
|
5401
|
+
method,
|
|
5384
5402
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
5385
5403
|
});
|
|
5386
5404
|
await handleGitLabError(response);
|
|
@@ -5430,7 +5448,11 @@ async function environmentRequest(projectId, path, method, body, query) {
|
|
|
5430
5448
|
for (const [key, value] of Object.entries(query ?? {}))
|
|
5431
5449
|
if (value !== undefined)
|
|
5432
5450
|
url.searchParams.set(key, String(value));
|
|
5433
|
-
const response = await fetch(url.toString(), {
|
|
5451
|
+
const response = await fetch(url.toString(), {
|
|
5452
|
+
...getFetchConfig(),
|
|
5453
|
+
method,
|
|
5454
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
5455
|
+
});
|
|
5434
5456
|
await handleGitLabError(response);
|
|
5435
5457
|
return response.status === 204 ? { success: true } : response.json();
|
|
5436
5458
|
}
|
|
@@ -5450,7 +5472,11 @@ async function triggerPipeline(projectId, token, ref, variables, inputs) {
|
|
|
5450
5472
|
body.variables = variables;
|
|
5451
5473
|
if (inputs)
|
|
5452
5474
|
body.inputs = inputs;
|
|
5453
|
-
const response = await fetch(url.toString(), {
|
|
5475
|
+
const response = await fetch(url.toString(), {
|
|
5476
|
+
...getFetchConfig(),
|
|
5477
|
+
method: "POST",
|
|
5478
|
+
body: Object.keys(body).length ? JSON.stringify(body) : undefined,
|
|
5479
|
+
});
|
|
5454
5480
|
await handleGitLabError(response);
|
|
5455
5481
|
return response.json();
|
|
5456
5482
|
}
|
|
@@ -6607,6 +6633,9 @@ async function getCurrentUser() {
|
|
|
6607
6633
|
}
|
|
6608
6634
|
}
|
|
6609
6635
|
}
|
|
6636
|
+
if (response.status === 403) {
|
|
6637
|
+
await throwUserApiScopeErrorUnlessRateLimitedAsync(response);
|
|
6638
|
+
}
|
|
6610
6639
|
await handleGitLabError(response);
|
|
6611
6640
|
throw new Error(`GitLab API error: ${response.status} ${response.statusText}`);
|
|
6612
6641
|
}
|
|
@@ -8021,8 +8050,7 @@ async function handleToolCall(params) {
|
|
|
8021
8050
|
}
|
|
8022
8051
|
case "push_files": {
|
|
8023
8052
|
const args = PushFilesSchema.parse(params.arguments);
|
|
8024
|
-
if (GITLAB_PERMISSION_MODE === "modify" &&
|
|
8025
|
-
fileOperationsIncludeDeleteOrMove(args.files)) {
|
|
8053
|
+
if (GITLAB_PERMISSION_MODE === "modify" && fileOperationsIncludeDeleteOrMove(args.files)) {
|
|
8026
8054
|
throw new Error("push_files does not allow delete or move actions in modify mode");
|
|
8027
8055
|
}
|
|
8028
8056
|
const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(f => ({
|
|
@@ -8527,6 +8555,9 @@ async function handleToolCall(params) {
|
|
|
8527
8555
|
const response = await fetch(url.toString(), {
|
|
8528
8556
|
...getFetchConfig(),
|
|
8529
8557
|
});
|
|
8558
|
+
if (response.status === 403) {
|
|
8559
|
+
await throwUserApiScopeErrorUnlessRateLimitedAsync(response);
|
|
8560
|
+
}
|
|
8530
8561
|
await handleGitLabError(response);
|
|
8531
8562
|
const data = await response.json();
|
|
8532
8563
|
const user = GitLabCurrentUserSchema.parse(data);
|
|
@@ -8893,7 +8924,14 @@ async function handleToolCall(params) {
|
|
|
8893
8924
|
const args = DeleteWorkItemEmojiReactionSchema.parse(params.arguments);
|
|
8894
8925
|
const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
|
|
8895
8926
|
const result = await removeGraphQLAwardEmoji(workItemGID, args.name);
|
|
8896
|
-
return {
|
|
8927
|
+
return {
|
|
8928
|
+
content: [
|
|
8929
|
+
{
|
|
8930
|
+
type: "text",
|
|
8931
|
+
text: JSON.stringify(result ?? { status: "success", message: "Work item emoji reaction removed" }),
|
|
8932
|
+
},
|
|
8933
|
+
],
|
|
8934
|
+
};
|
|
8897
8935
|
}
|
|
8898
8936
|
case "create_work_item_note_emoji_reaction": {
|
|
8899
8937
|
const args = CreateWorkItemNoteEmojiReactionSchema.parse(params.arguments);
|
|
@@ -8903,7 +8941,14 @@ async function handleToolCall(params) {
|
|
|
8903
8941
|
case "delete_work_item_note_emoji_reaction": {
|
|
8904
8942
|
const args = DeleteWorkItemNoteEmojiReactionSchema.parse(params.arguments);
|
|
8905
8943
|
const result = await removeGraphQLAwardEmoji(args.note_id, args.name);
|
|
8906
|
-
return {
|
|
8944
|
+
return {
|
|
8945
|
+
content: [
|
|
8946
|
+
{
|
|
8947
|
+
type: "text",
|
|
8948
|
+
text: JSON.stringify(result ?? { status: "success", message: "Work item note emoji reaction removed" }),
|
|
8949
|
+
},
|
|
8950
|
+
],
|
|
8951
|
+
};
|
|
8907
8952
|
}
|
|
8908
8953
|
case "get_timeline_events": {
|
|
8909
8954
|
const args = GetTimelineEventsSchema.parse(params.arguments);
|
|
@@ -9106,23 +9151,55 @@ async function handleToolCall(params) {
|
|
|
9106
9151
|
}
|
|
9107
9152
|
case "get_pipeline_variables": {
|
|
9108
9153
|
const { project_id, pipeline_id, page, per_page } = GetPipelineVariablesSchema.parse(params.arguments);
|
|
9109
|
-
return {
|
|
9154
|
+
return {
|
|
9155
|
+
content: [
|
|
9156
|
+
{
|
|
9157
|
+
type: "text",
|
|
9158
|
+
text: JSON.stringify(await getPipelineVariables(project_id, pipeline_id, page, per_page)),
|
|
9159
|
+
},
|
|
9160
|
+
],
|
|
9161
|
+
};
|
|
9110
9162
|
}
|
|
9111
9163
|
case "get_pipeline_test_report": {
|
|
9112
9164
|
const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
|
|
9113
|
-
return {
|
|
9165
|
+
return {
|
|
9166
|
+
content: [
|
|
9167
|
+
{
|
|
9168
|
+
type: "text",
|
|
9169
|
+
text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, false, page, per_page)),
|
|
9170
|
+
},
|
|
9171
|
+
],
|
|
9172
|
+
};
|
|
9114
9173
|
}
|
|
9115
9174
|
case "get_pipeline_test_report_summary": {
|
|
9116
9175
|
const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
|
|
9117
|
-
return {
|
|
9176
|
+
return {
|
|
9177
|
+
content: [
|
|
9178
|
+
{
|
|
9179
|
+
type: "text",
|
|
9180
|
+
text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, true, page, per_page)),
|
|
9181
|
+
},
|
|
9182
|
+
],
|
|
9183
|
+
};
|
|
9118
9184
|
}
|
|
9119
9185
|
case "delete_pipeline": {
|
|
9120
9186
|
const { project_id, pipeline_id } = DeletePipelineSchema.parse(params.arguments);
|
|
9121
|
-
return {
|
|
9187
|
+
return {
|
|
9188
|
+
content: [
|
|
9189
|
+
{ type: "text", text: JSON.stringify(await deletePipeline(project_id, pipeline_id)) },
|
|
9190
|
+
],
|
|
9191
|
+
};
|
|
9122
9192
|
}
|
|
9123
9193
|
case "update_pipeline_metadata": {
|
|
9124
9194
|
const { project_id, pipeline_id, name } = UpdatePipelineMetadataSchema.parse(params.arguments);
|
|
9125
|
-
return {
|
|
9195
|
+
return {
|
|
9196
|
+
content: [
|
|
9197
|
+
{
|
|
9198
|
+
type: "text",
|
|
9199
|
+
text: JSON.stringify(await updatePipelineMetadata(project_id, pipeline_id, name)),
|
|
9200
|
+
},
|
|
9201
|
+
],
|
|
9202
|
+
};
|
|
9126
9203
|
}
|
|
9127
9204
|
case "list_deployments": {
|
|
9128
9205
|
const args = ListDeploymentsSchema.parse(params.arguments);
|
|
@@ -9141,23 +9218,58 @@ async function handleToolCall(params) {
|
|
|
9141
9218
|
}
|
|
9142
9219
|
case "create_deployment": {
|
|
9143
9220
|
const { project_id, ...body } = CreateDeploymentSchema.parse(params.arguments);
|
|
9144
|
-
return {
|
|
9221
|
+
return {
|
|
9222
|
+
content: [
|
|
9223
|
+
{
|
|
9224
|
+
type: "text",
|
|
9225
|
+
text: JSON.stringify(await deploymentRequest(project_id, "", "POST", body)),
|
|
9226
|
+
},
|
|
9227
|
+
],
|
|
9228
|
+
};
|
|
9145
9229
|
}
|
|
9146
9230
|
case "update_deployment": {
|
|
9147
9231
|
const { project_id, deployment_id, ...body } = UpdateDeploymentSchema.parse(params.arguments);
|
|
9148
|
-
return {
|
|
9232
|
+
return {
|
|
9233
|
+
content: [
|
|
9234
|
+
{
|
|
9235
|
+
type: "text",
|
|
9236
|
+
text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "PUT", body)),
|
|
9237
|
+
},
|
|
9238
|
+
],
|
|
9239
|
+
};
|
|
9149
9240
|
}
|
|
9150
9241
|
case "delete_deployment": {
|
|
9151
9242
|
const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
|
|
9152
|
-
return {
|
|
9243
|
+
return {
|
|
9244
|
+
content: [
|
|
9245
|
+
{
|
|
9246
|
+
type: "text",
|
|
9247
|
+
text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "DELETE")),
|
|
9248
|
+
},
|
|
9249
|
+
],
|
|
9250
|
+
};
|
|
9153
9251
|
}
|
|
9154
9252
|
case "list_deployment_merge_requests": {
|
|
9155
9253
|
const { project_id, deployment_id, ...query } = ListDeploymentMergeRequestsSchema.parse(params.arguments);
|
|
9156
|
-
return {
|
|
9254
|
+
return {
|
|
9255
|
+
content: [
|
|
9256
|
+
{
|
|
9257
|
+
type: "text",
|
|
9258
|
+
text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/merge_requests`, "GET", undefined, query)),
|
|
9259
|
+
},
|
|
9260
|
+
],
|
|
9261
|
+
};
|
|
9157
9262
|
}
|
|
9158
9263
|
case "approve_deployment": {
|
|
9159
9264
|
const { project_id, deployment_id, ...body } = DeploymentApprovalSchema.parse(params.arguments);
|
|
9160
|
-
return {
|
|
9265
|
+
return {
|
|
9266
|
+
content: [
|
|
9267
|
+
{
|
|
9268
|
+
type: "text",
|
|
9269
|
+
text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/approval`, "POST", body)),
|
|
9270
|
+
},
|
|
9271
|
+
],
|
|
9272
|
+
};
|
|
9161
9273
|
}
|
|
9162
9274
|
case "list_environments": {
|
|
9163
9275
|
const args = ListEnvironmentsSchema.parse(params.arguments);
|
|
@@ -9176,47 +9288,124 @@ async function handleToolCall(params) {
|
|
|
9176
9288
|
}
|
|
9177
9289
|
case "update_environment": {
|
|
9178
9290
|
const { project_id, environment_id, ...body } = UpdateEnvironmentSchema.parse(params.arguments);
|
|
9179
|
-
return {
|
|
9291
|
+
return {
|
|
9292
|
+
content: [
|
|
9293
|
+
{
|
|
9294
|
+
type: "text",
|
|
9295
|
+
text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "PUT", body)),
|
|
9296
|
+
},
|
|
9297
|
+
],
|
|
9298
|
+
};
|
|
9180
9299
|
}
|
|
9181
9300
|
case "delete_environment": {
|
|
9182
9301
|
const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
|
|
9183
|
-
return {
|
|
9302
|
+
return {
|
|
9303
|
+
content: [
|
|
9304
|
+
{
|
|
9305
|
+
type: "text",
|
|
9306
|
+
text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "DELETE")),
|
|
9307
|
+
},
|
|
9308
|
+
],
|
|
9309
|
+
};
|
|
9184
9310
|
}
|
|
9185
9311
|
case "stop_environment": {
|
|
9186
9312
|
const { project_id, environment_id, force } = StopEnvironmentSchema.parse(params.arguments);
|
|
9187
|
-
return {
|
|
9313
|
+
return {
|
|
9314
|
+
content: [
|
|
9315
|
+
{
|
|
9316
|
+
type: "text",
|
|
9317
|
+
text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}/stop`, "POST", undefined, { force })),
|
|
9318
|
+
},
|
|
9319
|
+
],
|
|
9320
|
+
};
|
|
9188
9321
|
}
|
|
9189
9322
|
case "stop_stale_environments": {
|
|
9190
9323
|
const { project_id, before } = StopStaleEnvironmentsSchema.parse(params.arguments);
|
|
9191
|
-
return {
|
|
9324
|
+
return {
|
|
9325
|
+
content: [
|
|
9326
|
+
{
|
|
9327
|
+
type: "text",
|
|
9328
|
+
text: JSON.stringify(await environmentRequest(project_id, "/stop_stale", "POST", undefined, { before })),
|
|
9329
|
+
},
|
|
9330
|
+
],
|
|
9331
|
+
};
|
|
9192
9332
|
}
|
|
9193
9333
|
case "delete_review_app_environments": {
|
|
9194
9334
|
const { project_id, ...query } = DeleteReviewAppEnvironmentsSchema.parse(params.arguments);
|
|
9195
|
-
return {
|
|
9335
|
+
return {
|
|
9336
|
+
content: [
|
|
9337
|
+
{
|
|
9338
|
+
type: "text",
|
|
9339
|
+
text: JSON.stringify(await environmentRequest(project_id, "/review_apps", "DELETE", undefined, query)),
|
|
9340
|
+
},
|
|
9341
|
+
],
|
|
9342
|
+
};
|
|
9196
9343
|
}
|
|
9197
9344
|
case "list_pipeline_triggers": {
|
|
9198
9345
|
const { project_id } = ListPipelineTriggersSchema.parse(params.arguments);
|
|
9199
|
-
return {
|
|
9346
|
+
return {
|
|
9347
|
+
content: [
|
|
9348
|
+
{
|
|
9349
|
+
type: "text",
|
|
9350
|
+
text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id))),
|
|
9351
|
+
},
|
|
9352
|
+
],
|
|
9353
|
+
};
|
|
9200
9354
|
}
|
|
9201
9355
|
case "get_pipeline_trigger": {
|
|
9202
9356
|
const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
|
|
9203
|
-
return {
|
|
9357
|
+
return {
|
|
9358
|
+
content: [
|
|
9359
|
+
{
|
|
9360
|
+
type: "text",
|
|
9361
|
+
text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`))),
|
|
9362
|
+
},
|
|
9363
|
+
],
|
|
9364
|
+
};
|
|
9204
9365
|
}
|
|
9205
9366
|
case "create_pipeline_trigger": {
|
|
9206
9367
|
const { project_id, ...body } = CreatePipelineTriggerSchema.parse(params.arguments);
|
|
9207
|
-
return {
|
|
9368
|
+
return {
|
|
9369
|
+
content: [
|
|
9370
|
+
{
|
|
9371
|
+
type: "text",
|
|
9372
|
+
text: JSON.stringify(await pipelineTriggerRequest(project_id, "", "POST", body)),
|
|
9373
|
+
},
|
|
9374
|
+
],
|
|
9375
|
+
};
|
|
9208
9376
|
}
|
|
9209
9377
|
case "update_pipeline_trigger": {
|
|
9210
9378
|
const { project_id, trigger_id, ...body } = UpdatePipelineTriggerSchema.parse(params.arguments);
|
|
9211
|
-
return {
|
|
9379
|
+
return {
|
|
9380
|
+
content: [
|
|
9381
|
+
{
|
|
9382
|
+
type: "text",
|
|
9383
|
+
text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "PUT", body))),
|
|
9384
|
+
},
|
|
9385
|
+
],
|
|
9386
|
+
};
|
|
9212
9387
|
}
|
|
9213
9388
|
case "delete_pipeline_trigger": {
|
|
9214
9389
|
const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
|
|
9215
|
-
return {
|
|
9390
|
+
return {
|
|
9391
|
+
content: [
|
|
9392
|
+
{
|
|
9393
|
+
type: "text",
|
|
9394
|
+
text: JSON.stringify(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "DELETE")),
|
|
9395
|
+
},
|
|
9396
|
+
],
|
|
9397
|
+
};
|
|
9216
9398
|
}
|
|
9217
9399
|
case "trigger_pipeline": {
|
|
9218
9400
|
const { project_id, token, ref, variables, inputs } = TriggerPipelineSchema.parse(params.arguments);
|
|
9219
|
-
return {
|
|
9401
|
+
return {
|
|
9402
|
+
content: [
|
|
9403
|
+
{
|
|
9404
|
+
type: "text",
|
|
9405
|
+
text: JSON.stringify(await triggerPipeline(project_id, token, ref, variables, inputs)),
|
|
9406
|
+
},
|
|
9407
|
+
],
|
|
9408
|
+
};
|
|
9220
9409
|
}
|
|
9221
9410
|
case "list_pipeline_jobs": {
|
|
9222
9411
|
const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(params.arguments);
|
|
@@ -9583,7 +9772,7 @@ async function handleToolCall(params) {
|
|
|
9583
9772
|
};
|
|
9584
9773
|
}
|
|
9585
9774
|
case "play_pipeline_jobs": {
|
|
9586
|
-
const { project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds } = PlayPipelineJobsSchema.parse(params.arguments);
|
|
9775
|
+
const { project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds, } = PlayPipelineJobsSchema.parse(params.arguments);
|
|
9587
9776
|
const jobs = await playPipelineJobs(project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds);
|
|
9588
9777
|
return { content: [{ type: "text", text: JSON.stringify(jobs) }] };
|
|
9589
9778
|
}
|
|
@@ -9613,7 +9802,11 @@ async function handleToolCall(params) {
|
|
|
9613
9802
|
}
|
|
9614
9803
|
case "erase_pipeline_job": {
|
|
9615
9804
|
const { project_id, job_id } = ErasePipelineJobSchema.parse(params.arguments);
|
|
9616
|
-
return {
|
|
9805
|
+
return {
|
|
9806
|
+
content: [
|
|
9807
|
+
{ type: "text", text: JSON.stringify(await erasePipelineJob(project_id, job_id)) },
|
|
9808
|
+
],
|
|
9809
|
+
};
|
|
9617
9810
|
}
|
|
9618
9811
|
case "wait_for_pipeline": {
|
|
9619
9812
|
const { project_id, pipeline_id, timeout_seconds, poll_interval_seconds } = WaitForPipelineSchema.parse(params.arguments);
|
|
@@ -9645,7 +9838,15 @@ async function handleToolCall(params) {
|
|
|
9645
9838
|
}
|
|
9646
9839
|
const downloadUrl = buildDownloadUrl("job-artifacts", { project_id, job_id });
|
|
9647
9840
|
return {
|
|
9648
|
-
content: [
|
|
9841
|
+
content: [
|
|
9842
|
+
{
|
|
9843
|
+
type: "text",
|
|
9844
|
+
text: JSON.stringify({
|
|
9845
|
+
download_url: downloadUrl,
|
|
9846
|
+
filename: `artifacts_job_${job_id}.zip`,
|
|
9847
|
+
}),
|
|
9848
|
+
},
|
|
9849
|
+
],
|
|
9649
9850
|
};
|
|
9650
9851
|
}
|
|
9651
9852
|
const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
|
|
@@ -10148,7 +10349,12 @@ async function handleToolCall(params) {
|
|
|
10148
10349
|
filename: args.filename,
|
|
10149
10350
|
});
|
|
10150
10351
|
return {
|
|
10151
|
-
content: [
|
|
10352
|
+
content: [
|
|
10353
|
+
{
|
|
10354
|
+
type: "text",
|
|
10355
|
+
text: JSON.stringify({ download_url: downloadUrl, filename: args.filename }),
|
|
10356
|
+
},
|
|
10357
|
+
],
|
|
10152
10358
|
};
|
|
10153
10359
|
}
|
|
10154
10360
|
const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
|
|
@@ -10254,7 +10460,15 @@ async function handleToolCall(params) {
|
|
|
10254
10460
|
direct_asset_path: args.direct_asset_path,
|
|
10255
10461
|
});
|
|
10256
10462
|
return {
|
|
10257
|
-
content: [
|
|
10463
|
+
content: [
|
|
10464
|
+
{
|
|
10465
|
+
type: "text",
|
|
10466
|
+
text: JSON.stringify({
|
|
10467
|
+
download_url: downloadUrl,
|
|
10468
|
+
filename: args.direct_asset_path.split("/").pop() || args.direct_asset_path,
|
|
10469
|
+
}),
|
|
10470
|
+
},
|
|
10471
|
+
],
|
|
10258
10472
|
};
|
|
10259
10473
|
}
|
|
10260
10474
|
const assetContent = await downloadReleaseAsset(args.project_id, args.tag_name, args.direct_asset_path);
|
|
@@ -10437,17 +10651,13 @@ async function handleToolCall(params) {
|
|
|
10437
10651
|
HealthCheckSchema.parse(params.arguments ?? {});
|
|
10438
10652
|
const url = new URL(`${getEffectiveApiUrl()}/user`);
|
|
10439
10653
|
const response = await fetch(url.toString(), getFetchConfig());
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
}
|
|
10448
|
-
const versionMetadata = authenticated
|
|
10449
|
-
? await fetchGitLabVersionMetadata()
|
|
10450
|
-
: null;
|
|
10654
|
+
const jobAuthenticated = await authenticateWithJobTokenFallbackAsync(response);
|
|
10655
|
+
// insufficient_scope and insufficient_granular_scope mean the token was accepted
|
|
10656
|
+
// but User API scope is missing.
|
|
10657
|
+
// Rate-limit and authentication-ban 403s stay unauthenticated.
|
|
10658
|
+
const insufficientUserScope = !jobAuthenticated && (await isInsufficientUserScopeForbiddenAsync(response));
|
|
10659
|
+
const authenticated = response.ok || jobAuthenticated || insufficientUserScope;
|
|
10660
|
+
const versionMetadata = authenticated && !insufficientUserScope ? await fetchGitLabVersionMetadata() : null;
|
|
10451
10661
|
return {
|
|
10452
10662
|
content: [
|
|
10453
10663
|
{
|
|
@@ -10457,6 +10667,7 @@ async function handleToolCall(params) {
|
|
|
10457
10667
|
authenticated,
|
|
10458
10668
|
gitlab_url: getEffectiveApiUrl(),
|
|
10459
10669
|
mcp_server_version: SERVER_VERSION,
|
|
10670
|
+
...(insufficientUserScope ? { user_api_warning: USER_API_HEALTH_WARNING } : {}),
|
|
10460
10671
|
...(versionMetadata ?? {}),
|
|
10461
10672
|
}),
|
|
10462
10673
|
},
|
|
@@ -10510,7 +10721,9 @@ async function handleToolCall(params) {
|
|
|
10510
10721
|
});
|
|
10511
10722
|
await handleGitLabError(response);
|
|
10512
10723
|
return {
|
|
10513
|
-
content: [
|
|
10724
|
+
content: [
|
|
10725
|
+
{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }) },
|
|
10726
|
+
],
|
|
10514
10727
|
};
|
|
10515
10728
|
}
|
|
10516
10729
|
case "list_protected_branches": {
|
|
@@ -10585,7 +10798,12 @@ async function handleToolCall(params) {
|
|
|
10585
10798
|
});
|
|
10586
10799
|
await handleGitLabError(response);
|
|
10587
10800
|
return {
|
|
10588
|
-
content: [
|
|
10801
|
+
content: [
|
|
10802
|
+
{
|
|
10803
|
+
type: "text",
|
|
10804
|
+
text: JSON.stringify({ status: "unprotected", branch: args.branch_name }),
|
|
10805
|
+
},
|
|
10806
|
+
],
|
|
10589
10807
|
};
|
|
10590
10808
|
}
|
|
10591
10809
|
case "update_default_branch": {
|
|
@@ -10601,7 +10819,16 @@ async function handleToolCall(params) {
|
|
|
10601
10819
|
await handleGitLabError(response);
|
|
10602
10820
|
const data = await response.json();
|
|
10603
10821
|
return {
|
|
10604
|
-
content: [
|
|
10822
|
+
content: [
|
|
10823
|
+
{
|
|
10824
|
+
type: "text",
|
|
10825
|
+
text: JSON.stringify({
|
|
10826
|
+
status: "updated",
|
|
10827
|
+
default_branch: args.default_branch,
|
|
10828
|
+
project: data,
|
|
10829
|
+
}),
|
|
10830
|
+
},
|
|
10831
|
+
],
|
|
10605
10832
|
};
|
|
10606
10833
|
}
|
|
10607
10834
|
default:
|
|
@@ -10656,6 +10883,7 @@ function buildDownloadProxyDeps() {
|
|
|
10656
10883
|
* Start server with traditional SSE transport
|
|
10657
10884
|
*/
|
|
10658
10885
|
async function startSSEServer() {
|
|
10886
|
+
const { express, rateLimit, ipKeyGenerator, SSEServerTransport } = await loadRemoteHttpStack();
|
|
10659
10887
|
const app = express();
|
|
10660
10888
|
const sseAuthToken = getConfig("sse-auth-token", "SSE_AUTH_TOKEN");
|
|
10661
10889
|
if (MCP_TRUST_PROXY) {
|
|
@@ -10692,7 +10920,8 @@ async function startSSEServer() {
|
|
|
10692
10920
|
const host = req.headers.host;
|
|
10693
10921
|
const forwardedHost = MCP_TRUST_PROXY ? getForwardedRequestHost(req, true) : undefined;
|
|
10694
10922
|
const requestHosts = [host, forwardedHost].filter((value) => Boolean(value));
|
|
10695
|
-
if (requestHosts.length === 0 ||
|
|
10923
|
+
if (requestHosts.length === 0 ||
|
|
10924
|
+
!requestHosts.some(requestHost => effectiveHosts.has(requestHost))) {
|
|
10696
10925
|
res.status(403).json({ error: "Invalid Host header" });
|
|
10697
10926
|
return;
|
|
10698
10927
|
}
|
|
@@ -10867,6 +11096,7 @@ async function startSSEServer() {
|
|
|
10867
11096
|
* Start server with Streamable HTTP transport
|
|
10868
11097
|
*/
|
|
10869
11098
|
async function startStreamableHTTPServer() {
|
|
11099
|
+
const { express, rateLimit, ipKeyGenerator, StreamableHTTPServerTransport, mcpAuthRouter, requireBearerAuth, } = await loadRemoteHttpStack();
|
|
10870
11100
|
const app = express();
|
|
10871
11101
|
// Session IDs are untrusted map keys. A null prototype prevents inherited
|
|
10872
11102
|
// names such as "constructor" from masquerading as live transports.
|
|
@@ -11399,9 +11629,7 @@ async function startStreamableHTTPServer() {
|
|
|
11399
11629
|
// ipKeyGenerator for correct IPv6 subnet handling.
|
|
11400
11630
|
const rateLimitOptions = { keyGenerator: mcpRateLimitKeyGenerator };
|
|
11401
11631
|
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;
|
|
11632
|
+
const OAUTH_REGISTER_RATE_LIMIT_PER_HOUR = Number.isInteger(parsedRegisterLimit) && parsedRegisterLimit >= 1 ? parsedRegisterLimit : 20;
|
|
11405
11633
|
const clientRegistrationRateLimitOptions = {
|
|
11406
11634
|
...rateLimitOptions,
|
|
11407
11635
|
windowMs: 60 * 60 * 1000, // 1 hour
|
|
@@ -11501,9 +11729,7 @@ async function startStreamableHTTPServer() {
|
|
|
11501
11729
|
? staticMcpBearerAuth
|
|
11502
11730
|
: (_req, _res, next) => next();
|
|
11503
11731
|
const rejectUnknownStatefulSession = (req, res, next) => {
|
|
11504
|
-
const usesStatelessSessions = OAUTH_STATELESS_MODE &&
|
|
11505
|
-
STATELESS_MATERIAL &&
|
|
11506
|
-
(REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH);
|
|
11732
|
+
const usesStatelessSessions = OAUTH_STATELESS_MODE && STATELESS_MATERIAL && (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH);
|
|
11507
11733
|
const sessionId = readMcpSessionIdHeader(req);
|
|
11508
11734
|
if (!usesStatelessSessions && sessionId && !streamableTransports[sessionId]) {
|
|
11509
11735
|
res.status(404).json({ error: "Session not found" });
|
|
@@ -11521,7 +11747,9 @@ async function startStreamableHTTPServer() {
|
|
|
11521
11747
|
// entirely and derive the session auth from either the current request
|
|
11522
11748
|
// headers (init) or a sealed Mcp-Session-Id (subsequent requests).
|
|
11523
11749
|
// Rate limiting is disabled here because there is no shared counter.
|
|
11524
|
-
if (OAUTH_STATELESS_MODE &&
|
|
11750
|
+
if (OAUTH_STATELESS_MODE &&
|
|
11751
|
+
STATELESS_MATERIAL &&
|
|
11752
|
+
(REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
|
|
11525
11753
|
await handleStatelessMcpRequest(req, res, STATELESS_MATERIAL, OAUTH_STATELESS_SESSION_TTL_SECONDS);
|
|
11526
11754
|
return;
|
|
11527
11755
|
}
|
|
@@ -11802,7 +12030,9 @@ async function startStreamableHTTPServer() {
|
|
|
11802
12030
|
});
|
|
11803
12031
|
return;
|
|
11804
12032
|
}
|
|
11805
|
-
if (OAUTH_STATELESS_MODE &&
|
|
12033
|
+
if (OAUTH_STATELESS_MODE &&
|
|
12034
|
+
STATELESS_MATERIAL &&
|
|
12035
|
+
(REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
|
|
11806
12036
|
res.setHeader("Allow", "POST");
|
|
11807
12037
|
res.status(405).json({
|
|
11808
12038
|
error: "Method Not Allowed",
|
|
@@ -12059,16 +12289,16 @@ async function runServer() {
|
|
|
12059
12289
|
}
|
|
12060
12290
|
logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.join(", ")}`);
|
|
12061
12291
|
logger.info(`Default GitLab API URL: ${GITLAB_API_URL}`);
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
logger.warn(
|
|
12292
|
+
for (const warning of getDeprecatedEnvWarnings({
|
|
12293
|
+
readOnlyMode: GITLAB_READ_ONLY_MODE,
|
|
12294
|
+
permissionModeRaw: GITLAB_PERMISSION_MODE_RAW,
|
|
12295
|
+
allowedGroupsRaw: GITLAB_ALLOWED_GROUPS_RAW,
|
|
12296
|
+
oauthAllowedGroupsRaw: GITLAB_OAUTH_ALLOWED_GROUPS_RAW,
|
|
12297
|
+
useWikiRaw: USE_GITLAB_WIKI_RAW,
|
|
12298
|
+
useMilestoneRaw: USE_MILESTONE_RAW,
|
|
12299
|
+
usePipelineRaw: USE_PIPELINE_RAW,
|
|
12300
|
+
})) {
|
|
12301
|
+
logger.warn(warning);
|
|
12072
12302
|
}
|
|
12073
12303
|
if (GITLAB_OAUTH_ALLOWED_GROUPS) {
|
|
12074
12304
|
logger.info(`Group access control enabled for: ${GITLAB_OAUTH_ALLOWED_GROUPS.join(", ")}`);
|