@zereight/mcp-gitlab 2.1.17 → 2.1.19

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 CHANGED
@@ -187,6 +187,7 @@ MCP 서버가 직접 로컬 브라우저 callback을 받을 때만 `GITLAB_OAUTH
187
187
  | `STREAMABLE_HTTP` | 예 | 반드시 `true` |
188
188
  | `GITLAB_OAUTH_CALLBACK_PROXY` | 선택 | MCP 서버의 고정 `/callback` URL을 사용하려면 `true` |
189
189
  | `GITLAB_OAUTH_SCOPES` | 선택 | 쉼표로 구분된 scope 목록(기본값: `api,read_api,read_user`) |
190
+ | `GITLAB_ALLOWED_GROUPS` | 선택 | 쉼표로 구분된 GitLab 그룹 전체 경로 — 해당 그룹 및 하위 그룹 멤버만 토큰을 발급받을 수 있음 |
190
191
 
191
192
  > **`Unregistered redirect_uri` 문제 해결**
192
193
  >
package/README.md CHANGED
@@ -208,6 +208,7 @@ exchanging credentials with GitLab on behalf of the client.
208
208
  | `STREAMABLE_HTTP` | ✅ | Must be `true` |
209
209
  | `GITLAB_OAUTH_CALLBACK_PROXY` | optional | Set to `true` to use the MCP server's fixed `/callback` URL |
210
210
  | `GITLAB_OAUTH_SCOPES` | optional | Comma-separated scopes (default: `api,read_api,read_user`) |
211
+ | `GITLAB_ALLOWED_GROUPS` | optional | Comma-separated group full paths — only members (and subgroup members) may obtain a token |
211
212
 
212
213
  When `STREAMABLE_HTTP=true`, server-side `GITLAB_PERSONAL_ACCESS_TOKEN` or `GITLAB_JOB_TOKEN` require `REMOTE_AUTHORIZATION=true` or `GITLAB_MCP_OAUTH=true`.
213
214
 
@@ -657,6 +658,10 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
657
658
  164. `create_group_variable` - Create a new CI/CD variable in a group
658
659
  165. `update_group_variable` - Update an existing CI/CD variable in a group, with optional filter to disambiguate by environment scope
659
660
  166. `delete_group_variable` - Delete a CI/CD variable from a group, with optional filter to disambiguate by environment scope
661
+ 167. `get_dependency_proxy_settings` - Get dependency proxy settings for a group (enabled status, blob count, total size, image prefix, TTL policy)
662
+ 168. `update_dependency_proxy_settings` - Update dependency proxy settings for a group (enable/disable, credentials for authenticated Docker Hub pulls)
663
+ 169. `list_dependency_proxy_blobs` - List cached dependency proxy blobs for a group with cursor-based pagination
664
+ 170. `purge_dependency_proxy_cache` - Schedule purge of all cached dependency proxy blobs for a group
660
665
 
661
666
  <!-- TOOLS-END -->
662
667
 
package/README.zh-CN.md CHANGED
@@ -187,6 +187,7 @@ OpenCode、MCPJam、Claude.ai 等远程 MCP 客户端可能会在授权时发送
187
187
  | `STREAMABLE_HTTP` | 是 | 必须为 `true` |
188
188
  | `GITLAB_OAUTH_CALLBACK_PROXY` | 可选 | 设置为 `true` 时使用 MCP 服务器固定的 `/callback` URL |
189
189
  | `GITLAB_OAUTH_SCOPES` | 可选 | 逗号分隔的 scope(默认:`api,read_api,read_user`) |
190
+ | `GITLAB_ALLOWED_GROUPS` | 可选 | 逗号分隔的 GitLab 群组完整路径 — 仅该群组及其子群组的成员可获取令牌 |
190
191
 
191
192
  > **排查 `Unregistered redirect_uri`**
192
193
  >
package/build/config.js CHANGED
@@ -57,6 +57,13 @@ export const GITLAB_OAUTH_SCOPES = GITLAB_OAUTH_SCOPES_RAW
57
57
  ? GITLAB_OAUTH_SCOPES_RAW.split(",").map((s) => s.trim()).filter(Boolean)
58
58
  : undefined;
59
59
  export const GITLAB_OAUTH_CALLBACK_PROXY = getConfig("oauth-callback-proxy", "GITLAB_OAUTH_CALLBACK_PROXY") === "true";
60
+ export const GITLAB_ALLOWED_GROUPS = (() => {
61
+ const raw = getConfig("allowed-groups", "GITLAB_ALLOWED_GROUPS");
62
+ if (!raw)
63
+ return undefined;
64
+ const groups = raw.split(",").map((g) => g.trim()).filter(Boolean);
65
+ return groups.length > 0 ? groups : undefined;
66
+ })();
60
67
  export const ENABLE_DYNAMIC_API_URL = getConfig("enable-dynamic-api-url", "ENABLE_DYNAMIC_API_URL") === "true";
61
68
  // ---------------------------------------------------------------------------
62
69
  // Stateless mode (multi-pod safe OAuth / session encoding)
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, 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_READ_ONLY_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, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, } from "./config.js";
2
+ import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, 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_READ_ONLY_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, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_ALLOWED_GROUPS, } from "./config.js";
3
3
  /** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
4
4
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
5
5
  /**
@@ -134,12 +134,12 @@ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middlew
134
134
  import { GitLabClientPool } from "./gitlab-client-pool.js";
135
135
  import { allTools, readOnlyTools, destructiveTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
136
136
  import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
137
- CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateGroupSchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GitLabBlameEntrySchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
137
+ CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateGroupSchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GitLabBlameEntrySchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
138
138
  // Discussion Schemas
139
139
  GitLabDiscussionNoteSchema, // Added
140
140
  GitLabDiscussionSchema,
141
141
  // Draft Notes Schemas
142
- GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, GitLabCiVariableSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
142
+ GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, GitLabCiVariableSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GitLabDependencyProxySchema, GitLabDependencyProxyBlobSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
143
143
  GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
144
144
  import { randomUUID, createCipheriv, createDecipheriv, randomBytes, createHash } from "node:crypto";
145
145
  import { pino } from "pino";
@@ -5875,6 +5875,84 @@ async function deleteGroupVariable(groupId, key, filter) {
5875
5875
  const response = await fetch(url.toString(), { ...getFetchConfig(), method: "DELETE" });
5876
5876
  await handleGitLabError(response);
5877
5877
  }
5878
+ // --- Dependency Proxy ---
5879
+ async function resolveGroupFullPath(groupId) {
5880
+ const decoded = decodeURIComponent(groupId);
5881
+ if (/^\d+$/.test(decoded)) {
5882
+ const response = await fetch(`${getEffectiveApiUrl()}/groups/${decoded}`, getFetchConfig());
5883
+ await handleGitLabError(response);
5884
+ const data = z.object({ full_path: z.string() }).parse(await response.json());
5885
+ return data.full_path;
5886
+ }
5887
+ return decoded;
5888
+ }
5889
+ async function getDependencyProxySettings(groupPath) {
5890
+ const fullPath = await resolveGroupFullPath(groupPath);
5891
+ const data = await executeGraphQL(`query($fullPath: ID!) {
5892
+ group(fullPath: $fullPath) {
5893
+ dependencyProxySetting { enabled }
5894
+ dependencyProxyBlobCount
5895
+ dependencyProxyTotalSize
5896
+ dependencyProxyImagePrefix
5897
+ dependencyProxyImageTtlPolicy { enabled ttl }
5898
+ }
5899
+ }`, { fullPath });
5900
+ const g = data.group;
5901
+ if (!g)
5902
+ throw new Error(`Group not found: ${fullPath}`);
5903
+ return GitLabDependencyProxySchema.parse({
5904
+ enabled: g.dependencyProxySetting?.enabled ?? false,
5905
+ blob_count: g.dependencyProxyBlobCount,
5906
+ total_size: g.dependencyProxyTotalSize,
5907
+ image_prefix: g.dependencyProxyImagePrefix,
5908
+ ttl_policy: g.dependencyProxyImageTtlPolicy,
5909
+ });
5910
+ }
5911
+ async function updateDependencyProxySettings(groupPath, options) {
5912
+ if (options.enabled === undefined && options.identity === undefined && options.secret === undefined) {
5913
+ throw new Error("At least one of enabled, identity, or secret must be provided");
5914
+ }
5915
+ const fullPath = await resolveGroupFullPath(groupPath);
5916
+ const input = { groupPath: fullPath };
5917
+ if (options.enabled !== undefined)
5918
+ input["enabled"] = options.enabled;
5919
+ if (options.identity !== undefined)
5920
+ input["identity"] = options.identity;
5921
+ if (options.secret !== undefined)
5922
+ input["secret"] = options.secret;
5923
+ const mutationResult = await executeGraphQL(`mutation($input: UpdateDependencyProxySettingsInput!) {
5924
+ updateDependencyProxySettings(input: $input) { errors }
5925
+ }`, { input });
5926
+ const errors = mutationResult.updateDependencyProxySettings?.errors;
5927
+ if (errors && errors.length > 0) {
5928
+ throw new Error(`Failed to update dependency proxy settings: ${errors.join(", ")}`);
5929
+ }
5930
+ return getDependencyProxySettings(fullPath);
5931
+ }
5932
+ async function listDependencyProxyBlobs(groupPath, options = {}) {
5933
+ const fullPath = await resolveGroupFullPath(groupPath);
5934
+ const data = await executeGraphQL(`query($fullPath: ID!, $first: Int, $after: String) {
5935
+ group(fullPath: $fullPath) {
5936
+ dependencyProxyBlobs(first: $first, after: $after) {
5937
+ nodes { fileName size createdAt }
5938
+ pageInfo { hasNextPage endCursor }
5939
+ }
5940
+ }
5941
+ }`, { fullPath, first: options.first ?? 20, after: options.after });
5942
+ const conn = data.group?.dependencyProxyBlobs;
5943
+ if (!conn)
5944
+ throw new Error(`Group not found or dependency proxy not enabled: ${fullPath}`);
5945
+ return {
5946
+ blobs: conn.nodes.map(n => GitLabDependencyProxyBlobSchema.parse({ file_name: n.fileName, size: n.size, created_at: n.createdAt })),
5947
+ pageInfo: conn.pageInfo,
5948
+ };
5949
+ }
5950
+ async function purgeDependencyProxyCache(groupId) {
5951
+ const encoded = encodeURIComponent(decodeURIComponent(groupId));
5952
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/dependency_proxy/cache`);
5953
+ const response = await fetch(url.toString(), { ...getFetchConfig(), method: "DELETE" });
5954
+ await handleGitLabError(response);
5955
+ }
5878
5956
  /**
5879
5957
  * Upload a file to a GitLab project for use in markdown content.
5880
5958
  *
@@ -7921,6 +7999,45 @@ async function handleToolCall(params) {
7921
7999
  ],
7922
8000
  };
7923
8001
  }
8002
+ case "get_dependency_proxy_settings": {
8003
+ rejectIfProjectScopedDeployment("get_dependency_proxy_settings");
8004
+ const args = GetDependencyProxySettingsSchema.parse(params.arguments);
8005
+ const settings = await getDependencyProxySettings(args.group_id);
8006
+ return {
8007
+ content: [{ type: "text", text: JSON.stringify(settings, null, 2) }],
8008
+ };
8009
+ }
8010
+ case "update_dependency_proxy_settings": {
8011
+ rejectIfProjectScopedDeployment("update_dependency_proxy_settings");
8012
+ const args = UpdateDependencyProxySettingsSchema.parse(params.arguments);
8013
+ const { group_id, ...options } = args;
8014
+ const settings = await updateDependencyProxySettings(group_id, options);
8015
+ return {
8016
+ content: [{ type: "text", text: JSON.stringify(settings, null, 2) }],
8017
+ };
8018
+ }
8019
+ case "list_dependency_proxy_blobs": {
8020
+ rejectIfProjectScopedDeployment("list_dependency_proxy_blobs");
8021
+ const args = ListDependencyProxyBlobsSchema.parse(params.arguments);
8022
+ const { group_id, ...options } = args;
8023
+ const result = await listDependencyProxyBlobs(group_id, options);
8024
+ return {
8025
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
8026
+ };
8027
+ }
8028
+ case "purge_dependency_proxy_cache": {
8029
+ rejectIfProjectScopedDeployment("purge_dependency_proxy_cache");
8030
+ const args = PurgeDependencyProxyCacheSchema.parse(params.arguments);
8031
+ await purgeDependencyProxyCache(args.group_id);
8032
+ return {
8033
+ content: [
8034
+ {
8035
+ type: "text",
8036
+ text: JSON.stringify({ status: "success", message: "Dependency proxy cache purge scheduled" }, null, 2),
8037
+ },
8038
+ ],
8039
+ };
8040
+ }
7924
8041
  case "upload_markdown": {
7925
8042
  if (IS_REMOTE) {
7926
8043
  const args = MarkdownUploadRemoteSchema.parse(params.arguments);
@@ -8198,6 +8315,97 @@ async function handleToolCall(params) {
8198
8315
  content: [{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }, null, 2) }],
8199
8316
  };
8200
8317
  }
8318
+ case "list_protected_branches": {
8319
+ const args = ListProtectedBranchesSchema.parse(params.arguments);
8320
+ const projectId = decodeURIComponent(args.project_id);
8321
+ const effectiveProjectId = getEffectiveProjectId(projectId);
8322
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches`);
8323
+ if (args.search)
8324
+ url.searchParams.append("search", args.search);
8325
+ if (args.page)
8326
+ url.searchParams.append("page", String(args.page));
8327
+ if (args.per_page)
8328
+ url.searchParams.append("per_page", String(args.per_page));
8329
+ const response = await fetch(url.toString(), {
8330
+ ...getFetchConfig(),
8331
+ });
8332
+ await handleGitLabError(response);
8333
+ const data = z.array(GitLabProtectedBranchSchema).parse(await response.json());
8334
+ return {
8335
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8336
+ };
8337
+ }
8338
+ case "get_protected_branch": {
8339
+ const args = GetProtectedBranchSchema.parse(params.arguments);
8340
+ const projectId = decodeURIComponent(args.project_id);
8341
+ const effectiveProjectId = getEffectiveProjectId(projectId);
8342
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches/${encodeURIComponent(args.branch_name)}`);
8343
+ const response = await fetch(url.toString(), {
8344
+ ...getFetchConfig(),
8345
+ });
8346
+ await handleGitLabError(response);
8347
+ const data = GitLabProtectedBranchSchema.parse(await response.json());
8348
+ return {
8349
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8350
+ };
8351
+ }
8352
+ case "protect_branch": {
8353
+ const args = ProtectBranchSchema.parse(params.arguments);
8354
+ const projectId = decodeURIComponent(args.project_id);
8355
+ const effectiveProjectId = getEffectiveProjectId(projectId);
8356
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches`);
8357
+ const body = { name: args.branch_name };
8358
+ if (args.push_access_level !== undefined)
8359
+ body.push_access_level = args.push_access_level;
8360
+ if (args.merge_access_level !== undefined)
8361
+ body.merge_access_level = args.merge_access_level;
8362
+ if (args.unprotect_access_level !== undefined)
8363
+ body.unprotect_access_level = args.unprotect_access_level;
8364
+ if (args.allow_force_push !== undefined)
8365
+ body.allow_force_push = args.allow_force_push;
8366
+ if (args.code_owner_approval_required !== undefined)
8367
+ body.code_owner_approval_required = args.code_owner_approval_required;
8368
+ const response = await fetch(url.toString(), {
8369
+ ...getFetchConfig(),
8370
+ method: "POST",
8371
+ body: JSON.stringify(body),
8372
+ });
8373
+ await handleGitLabError(response);
8374
+ const data = GitLabProtectedBranchSchema.parse(await response.json());
8375
+ return {
8376
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8377
+ };
8378
+ }
8379
+ case "unprotect_branch": {
8380
+ const args = UnprotectBranchSchema.parse(params.arguments);
8381
+ const projectId = decodeURIComponent(args.project_id);
8382
+ const effectiveProjectId = getEffectiveProjectId(projectId);
8383
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/protected_branches/${encodeURIComponent(args.branch_name)}`);
8384
+ const response = await fetch(url.toString(), {
8385
+ ...getFetchConfig(),
8386
+ method: "DELETE",
8387
+ });
8388
+ await handleGitLabError(response);
8389
+ return {
8390
+ content: [{ type: "text", text: JSON.stringify({ status: "unprotected", branch: args.branch_name }, null, 2) }],
8391
+ };
8392
+ }
8393
+ case "update_default_branch": {
8394
+ const args = UpdateDefaultBranchSchema.parse(params.arguments);
8395
+ const projectId = decodeURIComponent(args.project_id);
8396
+ const effectiveProjectId = getEffectiveProjectId(projectId);
8397
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
8398
+ const response = await fetch(url.toString(), {
8399
+ ...getFetchConfig(),
8400
+ method: "PUT",
8401
+ body: JSON.stringify({ default_branch: args.default_branch }),
8402
+ });
8403
+ await handleGitLabError(response);
8404
+ const data = await response.json();
8405
+ return {
8406
+ content: [{ type: "text", text: JSON.stringify({ status: "updated", default_branch: args.default_branch, project: data }, null, 2) }],
8407
+ };
8408
+ }
8201
8409
  default:
8202
8410
  throw new Error(`Unknown tool: ${params.name}`);
8203
8411
  }
@@ -8602,9 +8810,9 @@ async function startStreamableHTTPServer() {
8602
8810
  return null;
8603
8811
  };
8604
8812
  /**
8605
- * Set or reset timeout for session auth
8813
+ * Set or reset timeout for session auth.
8606
8814
  * After SESSION_TIMEOUT_SECONDS of inactivity, the auth token is removed
8607
- * but the transport session remains active
8815
+ * and the Streamable HTTP transport is closed so the session slot is released.
8608
8816
  */
8609
8817
  const setAuthTimeout = (sessionId) => {
8610
8818
  // Clear existing timeout if any
@@ -8616,6 +8824,13 @@ async function startStreamableHTTPServer() {
8616
8824
  delete authBySession[sessionId];
8617
8825
  delete authTimeouts[sessionId];
8618
8826
  metrics.expiredSessions++;
8827
+ // Close the transport to free the slot; without this, stale sessions accumulate and exhaust MAX_SESSIONS.
8828
+ const transport = streamableTransports[sessionId];
8829
+ if (transport) {
8830
+ transport.close().catch(err => {
8831
+ logger.error(`Error closing transport for expired session ${sessionId}:`, err);
8832
+ });
8833
+ }
8619
8834
  }
8620
8835
  }, SESSION_TIMEOUT_SECONDS * 1000);
8621
8836
  };
@@ -8840,7 +9055,7 @@ async function startStreamableHTTPServer() {
8840
9055
  storedTtlSeconds: OAUTH_STATELESS_STORED_TTL_SECONDS,
8841
9056
  }
8842
9057
  : null;
8843
- const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, callbackUrl, statelessOptions);
9058
+ const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE, GITLAB_OAUTH_SCOPES, GITLAB_ALLOWED_GROUPS, GITLAB_OAUTH_CALLBACK_PROXY, callbackUrl, statelessOptions);
8844
9059
  const scopesSupported = GITLAB_OAUTH_SCOPES ?? ["api", "read_api", "read_user"];
8845
9060
  // When server URL has a path (e.g. behind Kong), the SDK's well-known metadata
8846
9061
  // advertises root-level endpoints. Override to use path-prefixed endpoints.
@@ -127,7 +127,11 @@ class GitLabOAuthServerProvider {
127
127
  // serialised into opaque OAuth values and the in-memory caches above are
128
128
  // bypassed. Enabled independently of callback-proxy mode.
129
129
  _stateless;
130
- constructor(gitlabBaseUrl, gitlabAppId, resourceName, readOnly, customScopes, callbackProxyEnabled = false, callbackUrl = "", stateless = null) {
130
+ // Group allowlist (optional). When set, tokens are rejected unless the
131
+ // authenticated user is a direct or inherited member of at least one group.
132
+ // Checked once at token issuance (exchangeAuthorizationCode), not per request.
133
+ _allowedGroups;
134
+ constructor(gitlabBaseUrl, gitlabAppId, resourceName, readOnly, customScopes, allowedGroups, callbackProxyEnabled = false, callbackUrl = "", stateless = null) {
131
135
  this._gitlabBaseUrl = gitlabBaseUrl;
132
136
  this._gitlabAppId = gitlabAppId;
133
137
  this._resourceName = resourceName;
@@ -137,6 +141,7 @@ class GitLabOAuthServerProvider {
137
141
  : readOnly
138
142
  ? REQUIRED_GITLAB_SCOPES_RO
139
143
  : REQUIRED_GITLAB_SCOPES_RW;
144
+ this._allowedGroups = allowedGroups ?? null;
140
145
  this._callbackProxyEnabled = callbackProxyEnabled;
141
146
  this._callbackUrl = callbackUrl;
142
147
  this._stateless = stateless;
@@ -313,6 +318,7 @@ class GitLabOAuthServerProvider {
313
318
  }
314
319
  // ---- Token exchange ----------------------------------------------------
315
320
  async exchangeAuthorizationCode(client, authorizationCode, codeVerifier, redirectUri, resource) {
321
+ let tokens;
316
322
  if (this._callbackProxyEnabled) {
317
323
  // --- Callback proxy mode ---
318
324
  // The authorizationCode is a proxy code we generated in handleCallback().
@@ -373,32 +379,74 @@ class GitLabOAuthServerProvider {
373
379
  throw new ServerError("PKCE verification failed");
374
380
  }
375
381
  }
376
- return entry.tokens;
382
+ tokens = entry.tokens;
377
383
  }
378
- // --- Passthrough mode (original behavior) ---
379
- const params = new URLSearchParams({
380
- grant_type: "authorization_code",
381
- client_id: this._gitlabAppId,
382
- code: authorizationCode,
383
- });
384
- if (codeVerifier)
385
- params.append("code_verifier", codeVerifier);
386
- if (redirectUri)
387
- params.append("redirect_uri", redirectUri);
388
- if (resource)
389
- params.append("resource", resource.href);
390
- const response = await fetch(`${this._gitlabBaseUrl}/oauth/token`, {
391
- method: "POST",
392
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
393
- body: params.toString(),
394
- });
395
- if (!response.ok) {
396
- const body = await response.text();
397
- logger.error(`Token exchange failed (${response.status}): ${body}`);
398
- throw new ServerError(`Token exchange failed: ${response.status}`);
384
+ else {
385
+ // --- Passthrough mode (original behavior) ---
386
+ const params = new URLSearchParams({
387
+ grant_type: "authorization_code",
388
+ client_id: this._gitlabAppId,
389
+ code: authorizationCode,
390
+ });
391
+ if (codeVerifier)
392
+ params.append("code_verifier", codeVerifier);
393
+ if (redirectUri)
394
+ params.append("redirect_uri", redirectUri);
395
+ if (resource)
396
+ params.append("resource", resource.href);
397
+ const response = await fetch(`${this._gitlabBaseUrl}/oauth/token`, {
398
+ method: "POST",
399
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
400
+ body: params.toString(),
401
+ });
402
+ if (!response.ok) {
403
+ const body = await response.text();
404
+ logger.error(`Token exchange failed (${response.status}): ${body}`);
405
+ throw new ServerError(`Token exchange failed: ${response.status}`);
406
+ }
407
+ const data = await response.json();
408
+ tokens = OAuthTokensSchema.parse(data);
399
409
  }
400
- const data = await response.json();
401
- return OAuthTokensSchema.parse(data);
410
+ if (this._allowedGroups) {
411
+ const isMember = await this._checkGroupMembership(tokens.access_token);
412
+ if (!isMember) {
413
+ logger.warn({ allowedGroups: this._allowedGroups }, "Token issuance denied: user is not a member of any allowed group");
414
+ throw new ServerError("Access denied: user is not a member of an allowed group");
415
+ }
416
+ }
417
+ return tokens;
418
+ }
419
+ /**
420
+ * Returns true if the token owner belongs to at least one group whose
421
+ * full_path equals or is a sub-path of any configured allowed group.
422
+ *
423
+ * Example: allowedGroups=["my-org"] allows members of "my-org",
424
+ * "my-org/team-a", "my-org/team-a/squad-1", etc.
425
+ */
426
+ async _checkGroupMembership(token) {
427
+ const allowedPaths = this._allowedGroups.map((g) => g.toLowerCase());
428
+ let page = 1;
429
+ while (true) {
430
+ const res = await fetch(`${this._gitlabBaseUrl}/api/v4/groups?min_access_level=10&per_page=100&page=${page}`, {
431
+ headers: { Authorization: `Bearer ${token}` }
432
+ });
433
+ if (!res.ok)
434
+ break;
435
+ const groups = (await res.json());
436
+ if (groups.length === 0)
437
+ break;
438
+ const matched = groups.some((g) => {
439
+ const fp = g.full_path.toLowerCase();
440
+ return allowedPaths.some((allowed) => fp === allowed || fp.startsWith(`${allowed}/`));
441
+ });
442
+ if (matched)
443
+ return true;
444
+ const totalPages = Number.parseInt(res.headers.get("x-total-pages") ?? "1", 10);
445
+ if (page >= totalPages)
446
+ break;
447
+ page++;
448
+ }
449
+ return false;
402
450
  }
403
451
  // ---- Refresh token -----------------------------------------------------
404
452
  async exchangeRefreshToken(_client, refreshToken, scopes, resource) {
@@ -598,6 +646,6 @@ class GitLabOAuthServerProvider {
598
646
  * callback-proxy state is encoded into opaque OAuth values
599
647
  * instead of an in-memory cache, enabling multi-pod deploys.
600
648
  */
601
- export function createGitLabOAuthProvider(gitlabBaseUrl, gitlabAppId, resourceName = "GitLab MCP Server", readOnly = false, customScopes, callbackProxyEnabled = false, callbackUrl = "", stateless = null) {
602
- return new GitLabOAuthServerProvider(gitlabBaseUrl, gitlabAppId, resourceName, readOnly, customScopes, callbackProxyEnabled, callbackUrl, stateless);
649
+ export function createGitLabOAuthProvider(gitlabBaseUrl, gitlabAppId, resourceName = "GitLab MCP Server", readOnly = false, customScopes, allowedGroups, callbackProxyEnabled = false, callbackUrl = "", stateless = null) {
650
+ return new GitLabOAuthServerProvider(gitlabBaseUrl, gitlabAppId, resourceName, readOnly, customScopes, allowedGroups, callbackProxyEnabled, callbackUrl, stateless);
603
651
  }
package/build/schemas.js CHANGED
@@ -1515,6 +1515,79 @@ export const ListBranchesSchema = ProjectParamsSchema.extend({
1515
1515
  export const DeleteBranchSchema = ProjectParamsSchema.extend({
1516
1516
  branch_name: z.string().describe("Name of the branch to delete"),
1517
1517
  });
1518
+ // Protected Branches related schemas
1519
+ export const ListProtectedBranchesSchema = ProjectParamsSchema.extend({
1520
+ search: z.string().optional().describe("Search term to filter protected branches by name"),
1521
+ }).merge(PaginationOptionsSchema);
1522
+ export const GetProtectedBranchSchema = ProjectParamsSchema.extend({
1523
+ branch_name: z.string().describe("Name of the protected branch"),
1524
+ });
1525
+ // String-aware boolean preprocessing: correctly handles "false" → false
1526
+ const stringBoolean = z.preprocess((val) => {
1527
+ if (typeof val === "string") {
1528
+ const lower = val.toLowerCase();
1529
+ if (lower === "false" || lower === "0")
1530
+ return false;
1531
+ if (lower === "true" || lower === "1")
1532
+ return true;
1533
+ }
1534
+ return val;
1535
+ }, z.boolean().optional());
1536
+ const protectedBranchAccessLevel = z.coerce
1537
+ .number()
1538
+ .int()
1539
+ .refine((level) => [0, 30, 40, 60].includes(level), {
1540
+ message: "Access level must be one of 0 (No access), 30 (Developer), 40 (Maintainer), or 60 (Admin)",
1541
+ });
1542
+ export const ProtectBranchSchema = z.preprocess((input) => {
1543
+ if (typeof input !== "object" || input === null) {
1544
+ return input;
1545
+ }
1546
+ const args = { ...input };
1547
+ if (!args.branch_name && args.name) {
1548
+ args.branch_name = args.name;
1549
+ }
1550
+ return args;
1551
+ }, ProjectParamsSchema.extend({
1552
+ branch_name: z.string().describe("Branch name or wildcard pattern to protect"),
1553
+ name: z
1554
+ .string()
1555
+ .optional()
1556
+ .describe("Deprecated alias for branch_name; prefer branch_name for consistency"),
1557
+ push_access_level: protectedBranchAccessLevel
1558
+ .optional()
1559
+ .describe("Access level for pushing (0=No access, 30=Developer, 40=Maintainer, 60=Admin). GitLab default applies when omitted."),
1560
+ merge_access_level: protectedBranchAccessLevel
1561
+ .optional()
1562
+ .describe("Access level for merging (0=No access, 30=Developer, 40=Maintainer, 60=Admin). GitLab default applies when omitted."),
1563
+ unprotect_access_level: protectedBranchAccessLevel
1564
+ .optional()
1565
+ .describe("Access level for unprotecting (0=No access, 30=Developer, 40=Maintainer, 60=Admin). GitLab default applies when omitted."),
1566
+ allow_force_push: stringBoolean.describe("Allow force push to the protected branch. Default: false"),
1567
+ code_owner_approval_required: stringBoolean.describe("Require code owner approval before merging (PREMIUM). Default: false"),
1568
+ }));
1569
+ export const UnprotectBranchSchema = ProjectParamsSchema.extend({
1570
+ branch_name: z.string().describe("Name of the protected branch to unprotect"),
1571
+ });
1572
+ // Update default branch schema
1573
+ export const UpdateDefaultBranchSchema = ProjectParamsSchema.extend({
1574
+ default_branch: z.string().describe("The new default branch name for the project"),
1575
+ });
1576
+ export const GitLabProtectedBranchAccessLevelSchema = z.object({
1577
+ access_level: z.number().nullable().optional(),
1578
+ access_level_description: z.string().optional(),
1579
+ user_id: z.number().optional(),
1580
+ group_id: z.number().optional(),
1581
+ });
1582
+ export const GitLabProtectedBranchSchema = z.object({
1583
+ id: z.number().optional(),
1584
+ name: z.string(),
1585
+ push_access_levels: z.array(GitLabProtectedBranchAccessLevelSchema).optional(),
1586
+ merge_access_levels: z.array(GitLabProtectedBranchAccessLevelSchema).optional(),
1587
+ unprotect_access_levels: z.array(GitLabProtectedBranchAccessLevelSchema).optional(),
1588
+ allow_force_push: z.boolean().optional(),
1589
+ code_owner_approval_required: z.boolean().optional(),
1590
+ });
1518
1591
  export const GitLabBranchSchema = z.object({
1519
1592
  name: z.string(),
1520
1593
  commit: z.object({
@@ -3503,3 +3576,39 @@ export const DeleteGroupVariableSchema = z.object({
3503
3576
  .optional()
3504
3577
  .describe("Filter by environment scope to disambiguate when multiple variables share the same key"),
3505
3578
  });
3579
+ // --- Dependency Proxy types ---
3580
+ export const GitLabDependencyProxySchema = z.object({
3581
+ enabled: z.boolean(),
3582
+ blob_count: z.number().nullable().optional(),
3583
+ total_size: z.string().nullable().optional(),
3584
+ image_prefix: z.string().nullable().optional(),
3585
+ ttl_policy: z
3586
+ .object({
3587
+ enabled: z.boolean(),
3588
+ ttl: z.number().nullable().optional(),
3589
+ })
3590
+ .nullable()
3591
+ .optional(),
3592
+ });
3593
+ export const GitLabDependencyProxyBlobSchema = z.object({
3594
+ file_name: z.string(),
3595
+ size: z.string(),
3596
+ created_at: z.string().nullable().optional(),
3597
+ });
3598
+ export const GetDependencyProxySettingsSchema = z.object({
3599
+ group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
3600
+ });
3601
+ export const UpdateDependencyProxySettingsSchema = z.object({
3602
+ group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
3603
+ enabled: z.boolean().optional().describe("Enable or disable the dependency proxy"),
3604
+ identity: z.string().optional().describe("Proxy username for authenticated Docker Hub pulls (Premium/Ultimate)"),
3605
+ secret: z.string().optional().describe("Proxy password / access token for authenticated pulls"),
3606
+ });
3607
+ export const ListDependencyProxyBlobsSchema = z.object({
3608
+ group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
3609
+ first: z.number().int().optional().describe("Number of blobs to return (default: 20)"),
3610
+ after: z.string().optional().describe("Cursor for pagination (from previous response pageInfo.endCursor)"),
3611
+ });
3612
+ export const PurgeDependencyProxyCacheSchema = z.object({
3613
+ group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
3614
+ });