@zereight/mcp-gitlab 2.1.41 → 2.1.43

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
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
80
80
 
81
81
  예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
82
82
 
83
- 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.40`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
83
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.42`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
84
84
 
85
85
  #### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
86
86
 
@@ -479,7 +479,7 @@ node build/index.js
479
479
  AI 에이전트가 skill/instruction 로딩을 지원하는 경우(Claude Code, GitHub Copilot, Cursor 등), [`skills/gitlab-mcp/`](./skills/gitlab-mcp/)에 미리 작성된 skill 파일을 사용할 수 있습니다.
480
480
 
481
481
  - **[SKILL.md](./skills/gitlab-mcp/SKILL.md)** — 도구셋 개요, 핵심 워크플로우, 파라미터 힌트를 담은 핵심 가이드
482
- - **[reference/](./skills/gitlab-mcp/reference/)** — 코드 리뷰, 머지 리퀘스트, 이슈, 파이프라인용 상세 워크플로우 문서
482
+ - **[reference/](./skills/gitlab-mcp/reference/)** — 코드 리뷰, 머지 리퀘스트, 이슈, 파이프라인, 취약점 트리아지용 상세 워크플로우 문서
483
483
 
484
484
  `skills` CLI로 설치:
485
485
 
@@ -491,7 +491,7 @@ AI 클라이언트에 skill 디렉터리를 등록하면 전체 ListTools 응답
491
491
 
492
492
  ## 도구 🛠️
493
493
 
494
- 전체 도구 목록은 영어 README의 [Tools 섹션](./README.md#tools-%EF%B8%8F)을 참고하세요. 현재 서버는 머지 리퀘스트, 이슈, 파이프라인, 배포, 환경, 아티팩트, 마일스톤, 위키, 저장소, 릴리스, 사용자, 이벤트, work item, 웹훅, 코드 검색, GraphQL 실행 도구를 제공합니다.
494
+ 전체 도구 목록은 영어 README의 [Tools 섹션](./README.md#tools-%EF%B8%8F)을 참고하세요. 현재 서버는 머지 리퀘스트, 이슈, 파이프라인, 배포, 환경, 아티팩트, 마일스톤, 위키, 저장소, 릴리스, 사용자, 이벤트, work item, 웹훅, 코드 검색, CI/CD 변수, dependency proxy, 취약점 트리아지, GraphQL 실행 도구를 제공합니다.
495
495
 
496
496
  ### Wiki 페이지 제목과 slug
497
497
 
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # GitLab MCP Server
2
2
 
3
+ [![MCP Toplist](https://mcptoplist.com/badge/io.github.zereight%2Fgitlab-mcp.svg)](https://mcptoplist.com/server/io.github.zereight%2Fgitlab-mcp)
4
+
3
5
  [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md)
4
6
 
5
7
  📖 **[Documentation →](https://zereight.github.io/gitlab-mcp/)** Setup guides, environment variables, and the full tool reference live on the hosted docs site.
@@ -80,7 +82,7 @@ npm install -g @zereight/mcp-gitlab
80
82
 
81
83
  The examples use `zereight-mcp-gitlab`, a less collision-prone alias for the legacy `mcp-gitlab` binary. If your MCP client cannot find it, use the absolute path from `which zereight-mcp-gitlab`.
82
84
 
83
- No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.40`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
85
+ No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.42`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
84
86
 
85
87
  #### Using CLI Arguments (for clients with env var issues)
86
88
 
@@ -523,7 +525,7 @@ No `headers` field is needed — Claude.ai obtains the token via OAuth automatic
523
525
  Pre-built skill files are available in [`skills/gitlab-mcp/`](./skills/gitlab-mcp/) for AI agents that support skill/instruction loading (Claude Code, GitHub Copilot, Cursor, etc.).
524
526
 
525
527
  - **[SKILL.md](./skills/gitlab-mcp/SKILL.md)** — Core guide (~800 tokens) with toolset overview, key workflows, and parameter hints
526
- - **[reference/](./skills/gitlab-mcp/reference/)** — Detailed workflow docs for code review, merge requests, issues, and pipelines
528
+ - **[reference/](./skills/gitlab-mcp/reference/)** — Detailed workflow docs for code review, merge requests, issues, pipelines, and vulnerability triage
527
529
 
528
530
  Install with the `skills` CLI:
529
531
 
@@ -751,7 +753,11 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
751
753
  209. `update_dependency_proxy_settings` - Update dependency proxy settings for a group (enable/disable, credentials for authenticated Docker Hub pulls)
752
754
  210. `list_dependency_proxy_blobs` - List cached dependency proxy blobs for a group with cursor-based pagination
753
755
  211. `purge_dependency_proxy_cache` - Schedule purge of all cached dependency proxy blobs for a group
754
- 212. `discover_tools` - Discover and activate additional tool categories for this session. Available categories: merge_requests, issues, repositories, branches, projects, labels, ci, groups, pipelines, milestones, wiki, releases, tags, users, workitems, webhooks, search, variables, dependency_proxy. Already-active categories are listed in the response.
756
+ 212. `list_project_vulnerabilities` - List vulnerabilities for a project with optional state, severity, and report type filters (GraphQL-backed, cursor pagination)
757
+ 213. `get_vulnerability` - Get full details of a specific vulnerability
758
+ 214. `dismiss_vulnerability` - Dismiss a vulnerability with a reason (acceptable_risk, false_positive, used_in_tests, mitigating_control, not_applicable) and optional comment
759
+ 215. `confirm_vulnerability` - Confirm a vulnerability as a real finding requiring remediation
760
+ 216. `discover_tools` - Discover and activate additional tool categories for this session. Available categories: merge_requests, issues, repositories, branches, projects, labels, ci, groups, pipelines, milestones, wiki, releases, tags, users, workitems, webhooks, search, variables, dependency_proxy, vulnerabilities. Already-active categories are listed in the response.
755
761
 
756
762
  <!-- TOOLS-END -->
757
763
 
package/README.zh-CN.md CHANGED
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
80
80
 
81
81
  示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
82
82
 
83
- 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.40`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
83
+ 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.42`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
84
84
 
85
85
  #### 使用 CLI 参数(适用于环境变量有问题的客户端)
86
86
 
@@ -479,7 +479,7 @@ node build/index.js
479
479
  对于支持 skill/instruction 加载的 AI 代理(Claude Code、GitHub Copilot、Cursor 等),[`skills/gitlab-mcp/`](./skills/gitlab-mcp/) 中提供了预构建 skill 文件。
480
480
 
481
481
  - **[SKILL.md](./skills/gitlab-mcp/SKILL.md)** — 核心指南,包含工具集概览、关键工作流和参数提示
482
- - **[reference/](./skills/gitlab-mcp/reference/)** — 代码审查、合并请求、议题和流水线的详细工作流文档
482
+ - **[reference/](./skills/gitlab-mcp/reference/)** — 代码审查、合并请求、议题、流水线和漏洞分类的详细工作流文档
483
483
 
484
484
  使用 `skills` CLI 安装:
485
485
 
@@ -491,7 +491,7 @@ npx skills add zereight/gitlab-mcp --skill gitlab-mcp-skill
491
491
 
492
492
  ## 工具 🛠️
493
493
 
494
- 完整工具列表请参考英文 README 的 [Tools 部分](./README.md#tools-%EF%B8%8F)。当前服务器提供合并请求、议题、流水线、部署、环境、制品、里程碑、Wiki、仓库、发布、用户、事件、work item、webhook、代码搜索和 GraphQL 执行相关工具。
494
+ 完整工具列表请参考英文 README 的 [Tools 部分](./README.md#tools-%EF%B8%8F)。当前服务器提供合并请求、议题、流水线、部署、环境、制品、里程碑、Wiki、仓库、发布、用户、事件、work item、webhook、代码搜索、CI/CD 变量、dependency proxy、漏洞分类和 GraphQL 执行相关工具。
495
495
 
496
496
  ### Wiki 页面标题与 slug
497
497
 
@@ -100,8 +100,9 @@ export function registerDownloadProxy(app, deps) {
100
100
  try {
101
101
  apiUrl = deps.resolveTrustedGitLabApiUrl(requestedApiUrl);
102
102
  }
103
- catch {
104
- res.status(400).json({ error: "Invalid X-GitLab-API-URL" });
103
+ catch (error) {
104
+ const message = error instanceof Error ? error.message : "Invalid X-GitLab-API-URL";
105
+ res.status(400).json({ error: message });
105
106
  return;
106
107
  }
107
108
  }
package/build/index.js CHANGED
@@ -90,6 +90,8 @@ import { graphqlQueryContainsWriteOperation, graphqlQueryContainsDeleteOperation
90
90
  import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
91
91
  import { redactSensitiveGitLabFields } from "./utils/redact-sensitive.js";
92
92
  import { checkForNewVersion } from "./utils/version-check.js";
93
+ import { assertGitLabVersionAtLeast } from "./utils/gitlab-version-gate.js";
94
+ import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
93
95
  import { cleanMutuallyExclusiveIdUsernameOptions, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
94
96
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
95
97
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
@@ -101,47 +103,11 @@ CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMerg
101
103
  GitLabDiscussionNoteSchema, // Added
102
104
  GitLabDiscussionSchema,
103
105
  // Draft Notes Schemas
104
- GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabGroupMilestonesSchema, 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
106
+ GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabGroupMilestonesSchema, 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, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
105
107
  GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, 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, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, 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";
106
108
  import { randomUUID, timingSafeEqual, } from "node:crypto";
107
- import { pino } from "pino";
108
- const logger = pino({
109
- level: process.env.LOG_LEVEL || "info",
110
- // Redact sensitive values that must never appear in logs. Covers both
111
- // typical auth-context property names and common HTTP header-bag shapes
112
- // that tool/fetch wrappers may include when logging errors.
113
- redact: {
114
- paths: [
115
- "token",
116
- "*.token",
117
- "ctx.token",
118
- "context.token",
119
- "authData.token",
120
- "auth.token",
121
- "headers.authorization",
122
- "headers.Authorization",
123
- 'headers["private-token"]',
124
- 'headers["Private-Token"]',
125
- 'headers["job-token"]',
126
- 'headers["JOB-TOKEN"]',
127
- 'headers["mcp-session-id"]',
128
- 'headers["Mcp-Session-Id"]',
129
- "sessionId",
130
- "*.sessionId",
131
- "ctx.sessionId",
132
- "context.sessionId",
133
- ],
134
- censor: "[REDACTED]",
135
- },
136
- transport: {
137
- target: "pino-pretty",
138
- options: {
139
- colorize: true,
140
- levelFirst: true,
141
- destination: 2,
142
- },
143
- },
144
- });
109
+ import { createLogger } from "./utils/logger.js";
110
+ const logger = createLogger();
145
111
  /**
146
112
  * Read version from package.json
147
113
  */
@@ -1144,7 +1110,8 @@ function resolveTrustedGitLabApiUrl(value) {
1144
1110
  }
1145
1111
  const allowedApiUrl = GITLAB_ALLOWED_API_URLS_BY_HOST.get(parsed.host);
1146
1112
  if (!allowedApiUrl) {
1147
- throw new Error(`GitLab API URL host is not allowed: ${parsed.host}`);
1113
+ throw new Error(`GitLab API URL host is not allowed: ${parsed.host}. ` +
1114
+ "Add the host to GITLAB_ALLOWED_HOSTS or GITLAB_API_URL.");
1148
1115
  }
1149
1116
  return allowedApiUrl;
1150
1117
  }
@@ -4424,19 +4391,49 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4424
4391
  };
4425
4392
  }
4426
4393
  }
4394
+ async function fetchGitLabInstanceVersion() {
4395
+ try {
4396
+ const response = await fetch(`${getEffectiveApiUrl()}/version`, {
4397
+ ...getFetchConfig(),
4398
+ });
4399
+ if (!response.ok)
4400
+ return null;
4401
+ const data = await response.json();
4402
+ if (typeof data === "object" &&
4403
+ data !== null &&
4404
+ "version" in data &&
4405
+ typeof data.version === "string") {
4406
+ return data.version;
4407
+ }
4408
+ return null;
4409
+ }
4410
+ catch {
4411
+ return null;
4412
+ }
4413
+ }
4427
4414
  /**
4428
4415
  * Publish all draft notes for a merge request
4429
4416
  * @param {string} projectId - The ID or URL-encoded path of the project
4430
4417
  * @param {number|string} mergeRequestIid - The internal ID of the merge request
4418
+ * @param {BulkPublishDraftNotesBody} options - Optional GitLab 19.2+ bulk_publish body fields
4431
4419
  * @returns {Promise<GitLabDiscussionNote[]>} Array of published notes
4432
4420
  */
4433
- async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
4421
+ async function bulkPublishDraftNotes(projectId, mergeRequestIid, options = {}) {
4434
4422
  projectId = decodeURIComponent(projectId);
4435
4423
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/bulk_publish`);
4424
+ const body = buildBulkPublishDraftNotesBody(options);
4425
+ if (needsGitLab19_2BulkPublish(body)) {
4426
+ await assertGitLabVersionAtLeast({
4427
+ major: 19,
4428
+ minor: 2,
4429
+ feature: "reviewer_state, note, and internal on bulk_publish_draft_notes",
4430
+ retryHint: "Omit reviewer_state, note, and internal, then retry.",
4431
+ }, fetchGitLabInstanceVersion);
4432
+ }
4436
4433
  const response = await fetch(url.toString(), {
4437
4434
  ...getFetchConfig(),
4438
4435
  method: "POST", // Changed from PUT to POST
4439
- body: JSON.stringify({}), // Send empty body for POST request
4436
+ body: JSON.stringify(body),
4440
4437
  });
4441
4438
  if (!response.ok) {
4442
4439
  const errorText = await response.text();
@@ -6421,6 +6418,262 @@ async function purgeDependencyProxyCache(groupId) {
6421
6418
  const response = await fetch(url.toString(), { ...getFetchConfig(), method: "DELETE" });
6422
6419
  await handleGitLabError(response);
6423
6420
  }
6421
+ // --- Vulnerability functions (GraphQL) ---
6422
+ //
6423
+ // The REST Vulnerabilities API is deprecated and silently ignores filter
6424
+ // params on list and reason/comment on dismiss. GitLab recommends the
6425
+ // GraphQL API for vulnerability management, which supports all of these.
6426
+ /** Convert a numeric vulnerability ID (or an existing Vulnerability GID) to a GraphQL global ID. */
6427
+ function toVulnerabilityGid(vulnerabilityId) {
6428
+ if (/^\d+$/.test(vulnerabilityId)) {
6429
+ return `gid://gitlab/Vulnerability/${vulnerabilityId}`;
6430
+ }
6431
+ if (/^gid:\/\/gitlab\/Vulnerability\/\d+$/.test(vulnerabilityId)) {
6432
+ return vulnerabilityId;
6433
+ }
6434
+ throw new Error(`Invalid vulnerability ID "${vulnerabilityId}": expected a numeric ID or a gid://gitlab/Vulnerability/<id> global ID`);
6435
+ }
6436
+ /**
6437
+ * Resolve a project ID or path to the full namespace path required by
6438
+ * GraphQL's `project(fullPath:)` query. Numeric IDs are resolved via a
6439
+ * REST lookup; paths are returned as-is (decoded).
6440
+ */
6441
+ async function resolveProjectFullPath(projectId) {
6442
+ const decoded = decodeURIComponent(getEffectiveProjectId(decodeURIComponent(projectId)));
6443
+ if (!/^\d+$/.test(decoded)) {
6444
+ return decoded;
6445
+ }
6446
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(decoded)}`);
6447
+ const response = await fetch(url.toString(), { ...getFetchConfig() });
6448
+ await handleGitLabError(response);
6449
+ const project = (await response.json());
6450
+ if (!project.path_with_namespace) {
6451
+ throw new Error(`Could not resolve full path for project ${decoded}`);
6452
+ }
6453
+ return project.path_with_namespace;
6454
+ }
6455
+ /**
6456
+ * Enforce GITLAB_ALLOWED_PROJECT_IDS for vulnerability tools. Vulnerability
6457
+ * GIDs are globally unique rather than project-scoped, so the boundary must
6458
+ * be checked against the project the vulnerability actually belongs to —
6459
+ * mirroring what getEffectiveProjectId does for project-scoped REST calls.
6460
+ * Allowlist entries may be numeric IDs or full namespace paths.
6461
+ */
6462
+ function assertVulnerabilityProjectAllowed(vulnerabilityId, project) {
6463
+ if (GITLAB_ALLOWED_PROJECT_IDS.length === 0) {
6464
+ return;
6465
+ }
6466
+ const fullPath = project?.fullPath ?? undefined;
6467
+ const numericId = project?.id?.match(/^gid:\/\/gitlab\/Project\/(\d+)$/)?.[1];
6468
+ const allowed = (fullPath !== undefined && GITLAB_ALLOWED_PROJECT_IDS.includes(fullPath)) ||
6469
+ (numericId !== undefined && GITLAB_ALLOWED_PROJECT_IDS.includes(numericId));
6470
+ if (!allowed) {
6471
+ throw new Error(`Access denied: Vulnerability ${vulnerabilityId} belongs to project ${fullPath ?? numericId ?? "unknown"}, which is not in the allowed project list: ${GITLAB_ALLOWED_PROJECT_IDS.join(", ")}`);
6472
+ }
6473
+ }
6474
+ /**
6475
+ * Pre-flight allowlist check for vulnerability mutations: resolves the
6476
+ * vulnerability's project and verifies it against GITLAB_ALLOWED_PROJECT_IDS
6477
+ * before any write is issued. No-op (no extra request) when the allowlist
6478
+ * is not configured.
6479
+ */
6480
+ async function ensureVulnerabilityProjectAllowed(vulnerabilityId) {
6481
+ if (GITLAB_ALLOWED_PROJECT_IDS.length === 0) {
6482
+ return;
6483
+ }
6484
+ const data = await executeGraphQL(`query getVulnerabilityProject($id: VulnerabilityID!) {
6485
+ vulnerability(id: $id) {
6486
+ project {
6487
+ id
6488
+ fullPath
6489
+ }
6490
+ }
6491
+ }`, { id: toVulnerabilityGid(vulnerabilityId) });
6492
+ if (!data.vulnerability) {
6493
+ throw new Error(`Vulnerability not found: ${vulnerabilityId}`);
6494
+ }
6495
+ assertVulnerabilityProjectAllowed(vulnerabilityId, data.vulnerability.project);
6496
+ }
6497
+ /** Shared GraphQL selection set for vulnerability objects. */
6498
+ const VULNERABILITY_FIELDS = `
6499
+ id
6500
+ title
6501
+ description
6502
+ state
6503
+ severity
6504
+ reportType
6505
+ detectedAt
6506
+ confirmedAt
6507
+ resolvedAt
6508
+ dismissedAt
6509
+ dismissalReason
6510
+ webUrl
6511
+ scanner {
6512
+ name
6513
+ externalId
6514
+ vendor
6515
+ }
6516
+ identifiers {
6517
+ externalType
6518
+ externalId
6519
+ name
6520
+ url
6521
+ }
6522
+ links {
6523
+ name
6524
+ url
6525
+ }
6526
+ location {
6527
+ ... on VulnerabilityLocationSast {
6528
+ file
6529
+ startLine
6530
+ endLine
6531
+ }
6532
+ ... on VulnerabilityLocationSecretDetection {
6533
+ file
6534
+ startLine
6535
+ endLine
6536
+ }
6537
+ ... on VulnerabilityLocationDependencyScanning {
6538
+ file
6539
+ dependency {
6540
+ package {
6541
+ name
6542
+ }
6543
+ version
6544
+ }
6545
+ }
6546
+ ... on VulnerabilityLocationContainerScanning {
6547
+ image
6548
+ operatingSystem
6549
+ dependency {
6550
+ package {
6551
+ name
6552
+ }
6553
+ version
6554
+ }
6555
+ }
6556
+ ... on VulnerabilityLocationDast {
6557
+ path
6558
+ hostname
6559
+ }
6560
+ }
6561
+ `;
6562
+ async function listProjectVulnerabilities(projectId, options = {}) {
6563
+ const fullPath = await resolveProjectFullPath(projectId);
6564
+ const variables = {
6565
+ fullPath,
6566
+ first: Math.min(options.first ?? 20, 100),
6567
+ };
6568
+ if (options.state)
6569
+ variables.state = [options.state.toUpperCase()];
6570
+ if (options.severity)
6571
+ variables.severity = [options.severity.toUpperCase()];
6572
+ if (options.report_type)
6573
+ variables.reportType = [options.report_type.toUpperCase()];
6574
+ if (options.after)
6575
+ variables.after = options.after;
6576
+ const data = await executeGraphQL(`query listProjectVulnerabilities(
6577
+ $fullPath: ID!
6578
+ $state: [VulnerabilityState!]
6579
+ $severity: [VulnerabilitySeverity!]
6580
+ $reportType: [VulnerabilityReportType!]
6581
+ $first: Int
6582
+ $after: String
6583
+ ) {
6584
+ project(fullPath: $fullPath) {
6585
+ vulnerabilities(
6586
+ state: $state
6587
+ severity: $severity
6588
+ reportType: $reportType
6589
+ first: $first
6590
+ after: $after
6591
+ ) {
6592
+ nodes {
6593
+ ${VULNERABILITY_FIELDS}
6594
+ }
6595
+ pageInfo {
6596
+ endCursor
6597
+ hasNextPage
6598
+ }
6599
+ }
6600
+ }
6601
+ }`, variables);
6602
+ if (!data.project) {
6603
+ throw new Error(`Project not found or not accessible: ${fullPath}`);
6604
+ }
6605
+ return {
6606
+ vulnerabilities: data.project.vulnerabilities.nodes,
6607
+ pageInfo: data.project.vulnerabilities.pageInfo,
6608
+ };
6609
+ }
6610
+ async function getVulnerability(vulnerabilityId) {
6611
+ const data = await executeGraphQL(`query getVulnerability($id: VulnerabilityID!) {
6612
+ vulnerability(id: $id) {
6613
+ ${VULNERABILITY_FIELDS}
6614
+ project {
6615
+ id
6616
+ name
6617
+ fullPath
6618
+ }
6619
+ }
6620
+ }`, { id: toVulnerabilityGid(vulnerabilityId) });
6621
+ if (!data.vulnerability) {
6622
+ throw new Error(`Vulnerability not found: ${vulnerabilityId}`);
6623
+ }
6624
+ assertVulnerabilityProjectAllowed(vulnerabilityId, data.vulnerability.project);
6625
+ return data.vulnerability;
6626
+ }
6627
+ async function dismissVulnerability(vulnerabilityId, reason, comment) {
6628
+ await ensureVulnerabilityProjectAllowed(vulnerabilityId);
6629
+ const input = {
6630
+ id: toVulnerabilityGid(vulnerabilityId),
6631
+ dismissalReason: reason.toUpperCase(),
6632
+ };
6633
+ if (comment)
6634
+ input.comment = comment;
6635
+ const data = await executeGraphQL(`mutation dismissVulnerability($input: VulnerabilityDismissInput!) {
6636
+ vulnerabilityDismiss(input: $input) {
6637
+ vulnerability {
6638
+ id
6639
+ state
6640
+ dismissedAt
6641
+ dismissalReason
6642
+ }
6643
+ errors
6644
+ }
6645
+ }`, { input });
6646
+ if (data.vulnerabilityDismiss.errors?.length) {
6647
+ throw new Error(`Failed to dismiss vulnerability: ${data.vulnerabilityDismiss.errors.join(", ")}`);
6648
+ }
6649
+ if (!data.vulnerabilityDismiss.vulnerability) {
6650
+ throw new Error(`Vulnerability not returned after dismissal (id: ${vulnerabilityId})`);
6651
+ }
6652
+ return data.vulnerabilityDismiss.vulnerability;
6653
+ }
6654
+ async function confirmVulnerability(vulnerabilityId, comment) {
6655
+ await ensureVulnerabilityProjectAllowed(vulnerabilityId);
6656
+ const input = { id: toVulnerabilityGid(vulnerabilityId) };
6657
+ if (comment)
6658
+ input.comment = comment;
6659
+ const data = await executeGraphQL(`mutation confirmVulnerability($input: VulnerabilityConfirmInput!) {
6660
+ vulnerabilityConfirm(input: $input) {
6661
+ vulnerability {
6662
+ id
6663
+ state
6664
+ confirmedAt
6665
+ }
6666
+ errors
6667
+ }
6668
+ }`, { input });
6669
+ if (data.vulnerabilityConfirm.errors?.length) {
6670
+ throw new Error(`Failed to confirm vulnerability: ${data.vulnerabilityConfirm.errors.join(", ")}`);
6671
+ }
6672
+ if (!data.vulnerabilityConfirm.vulnerability) {
6673
+ throw new Error(`Vulnerability not returned after confirmation (id: ${vulnerabilityId})`);
6674
+ }
6675
+ return data.vulnerabilityConfirm.vulnerability;
6676
+ }
6424
6677
  /**
6425
6678
  * Upload a file to a GitLab project for use in markdown content.
6426
6679
  *
@@ -7612,8 +7865,8 @@ async function handleToolCall(params) {
7612
7865
  }
7613
7866
  case "bulk_publish_draft_notes": {
7614
7867
  const args = BulkPublishDraftNotesSchema.parse(params.arguments);
7615
- const { project_id, merge_request_iid } = args;
7616
- const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid);
7868
+ const { project_id, merge_request_iid, ...options } = args;
7869
+ const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid, options);
7617
7870
  return {
7618
7871
  content: [{ type: "text", text: JSON.stringify(publishedNotes) }],
7619
7872
  };
@@ -8837,6 +9090,28 @@ async function handleToolCall(params) {
8837
9090
  ],
8838
9091
  };
8839
9092
  }
9093
+ // --- Vulnerability tools ---
9094
+ case "list_project_vulnerabilities": {
9095
+ const args = ListProjectVulnerabilitiesSchema.parse(params.arguments);
9096
+ const { project_id, ...options } = args;
9097
+ const result = await listProjectVulnerabilities(project_id, options);
9098
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
9099
+ }
9100
+ case "get_vulnerability": {
9101
+ const args = GetVulnerabilitySchema.parse(params.arguments);
9102
+ const result = await getVulnerability(args.vulnerability_id);
9103
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
9104
+ }
9105
+ case "dismiss_vulnerability": {
9106
+ const args = DismissVulnerabilitySchema.parse(params.arguments);
9107
+ const result = await dismissVulnerability(args.vulnerability_id, args.reason, args.comment);
9108
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
9109
+ }
9110
+ case "confirm_vulnerability": {
9111
+ const args = ConfirmVulnerabilitySchema.parse(params.arguments);
9112
+ const result = await confirmVulnerability(args.vulnerability_id, args.comment);
9113
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
9114
+ }
8840
9115
  case "upload_markdown": {
8841
9116
  if (IS_REMOTE) {
8842
9117
  const args = MarkdownUploadRemoteSchema.parse(params.arguments);
@@ -9464,8 +9739,9 @@ async function startStreamableHTTPServer() {
9464
9739
  try {
9465
9740
  apiUrl = resolveTrustedGitLabApiUrl(dynamicApiUrl);
9466
9741
  }
9467
- catch {
9468
- logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. Auth will fail.`);
9742
+ catch (error) {
9743
+ const reason = error instanceof Error ? error.message : "Invalid X-GitLab-API-URL";
9744
+ logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. ${reason}`);
9469
9745
  return null; // Reject if URL is malformed or not allowed
9470
9746
  }
9471
9747
  }
@@ -47,11 +47,11 @@
47
47
  import { InvalidTokenError, ServerError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
48
48
  import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
49
49
  import { randomUUID, randomBytes, createHash } from "node:crypto";
50
- import { pino } from "pino";
51
50
  import { looksLikeStatelessClientId, mintClientId, openClientId, } from "./stateless/client-id.js";
52
51
  import { looksLikeStatelessState, mintPendingAuthState, openPendingAuthState, } from "./stateless/pending-auth.js";
53
52
  import { looksLikeStatelessStoredTokensCode, mintStoredTokensCode, openStoredTokensCode, } from "./stateless/stored-tokens.js";
54
- const logger = pino({ name: "gitlab-mcp-oauth-proxy" });
53
+ import { createLogger } from "./utils/logger.js";
54
+ const logger = createLogger("gitlab-mcp-oauth-proxy");
55
55
  // ---------------------------------------------------------------------------
56
56
  // GitLab OAuth Server Provider
57
57
  // ---------------------------------------------------------------------------
package/build/oauth.js CHANGED
@@ -9,11 +9,8 @@ import { execFile } from "child_process";
9
9
  import { promisify } from "util";
10
10
  import open from "open";
11
11
  import pkceChallenge from "pkce-challenge";
12
- import { pino } from "pino";
13
- const logger = pino({
14
- name: "gitlab-mcp-oauth",
15
- level: process.env.LOG_LEVEL || "info",
16
- }, pino.destination(2));
12
+ import { createLogger } from "./utils/logger.js";
13
+ const logger = createLogger("gitlab-mcp-oauth");
17
14
  const execFileAsync = promisify(execFile);
18
15
  function escapeHtml(str) {
19
16
  const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };