@zereight/mcp-gitlab 2.1.42 → 2.1.44

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.41`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
83
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.43`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `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.41`. 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.43`. 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
 
@@ -698,7 +700,7 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
698
700
  156. `list_group_iterations` - List group iterations with filtering options
699
701
  157. `upload_markdown` - Upload a file to a GitLab project for use in markdown content
700
702
  158. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
701
- 159. `health_check` - Verify server status and authentication
703
+ 159. `health_check` - Verify server status and authentication; when authenticated, reports GitLab instance version from `/api/v4/version` (`version`, `revision`, `enterprise`)
702
704
  160. `list_events` - List all events for the currently authenticated user
703
705
  161. `get_project_events` - List all visible events for a specified project
704
706
  162. `list_releases` - List all releases for a project
@@ -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.41`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
83
+ 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.43`。如果始终想使用最新版本,请改用 `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
@@ -91,6 +91,7 @@ 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
93
  import { assertGitLabVersionAtLeast } from "./utils/gitlab-version-gate.js";
94
+ import { parseGitLabVersionApiResponse, } from "./utils/gitlab-instance-version.js";
94
95
  import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
95
96
  import { cleanMutuallyExclusiveIdUsernameOptions, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
96
97
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
@@ -103,47 +104,11 @@ CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMerg
103
104
  GitLabDiscussionNoteSchema, // Added
104
105
  GitLabDiscussionSchema,
105
106
  // Draft Notes Schemas
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, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
107
+ 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
107
108
  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";
108
109
  import { randomUUID, timingSafeEqual, } from "node:crypto";
109
- import { pino } from "pino";
110
- const logger = pino({
111
- level: process.env.LOG_LEVEL || "info",
112
- // Redact sensitive values that must never appear in logs. Covers both
113
- // typical auth-context property names and common HTTP header-bag shapes
114
- // that tool/fetch wrappers may include when logging errors.
115
- redact: {
116
- paths: [
117
- "token",
118
- "*.token",
119
- "ctx.token",
120
- "context.token",
121
- "authData.token",
122
- "auth.token",
123
- "headers.authorization",
124
- "headers.Authorization",
125
- 'headers["private-token"]',
126
- 'headers["Private-Token"]',
127
- 'headers["job-token"]',
128
- 'headers["JOB-TOKEN"]',
129
- 'headers["mcp-session-id"]',
130
- 'headers["Mcp-Session-Id"]',
131
- "sessionId",
132
- "*.sessionId",
133
- "ctx.sessionId",
134
- "context.sessionId",
135
- ],
136
- censor: "[REDACTED]",
137
- },
138
- transport: {
139
- target: "pino-pretty",
140
- options: {
141
- colorize: true,
142
- levelFirst: true,
143
- destination: 2,
144
- },
145
- },
146
- });
110
+ import { createLogger } from "./utils/logger.js";
111
+ const logger = createLogger();
147
112
  /**
148
113
  * Read version from package.json
149
114
  */
@@ -1146,7 +1111,8 @@ function resolveTrustedGitLabApiUrl(value) {
1146
1111
  }
1147
1112
  const allowedApiUrl = GITLAB_ALLOWED_API_URLS_BY_HOST.get(parsed.host);
1148
1113
  if (!allowedApiUrl) {
1149
- throw new Error(`GitLab API URL host is not allowed: ${parsed.host}`);
1114
+ throw new Error(`GitLab API URL host is not allowed: ${parsed.host}. ` +
1115
+ "Add the host to GITLAB_ALLOWED_HOSTS or GITLAB_API_URL.");
1150
1116
  }
1151
1117
  return allowedApiUrl;
1152
1118
  }
@@ -4426,7 +4392,7 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4426
4392
  };
4427
4393
  }
4428
4394
  }
4429
- async function fetchGitLabInstanceVersion() {
4395
+ async function fetchGitLabVersionMetadata() {
4430
4396
  try {
4431
4397
  const response = await fetch(`${getEffectiveApiUrl()}/version`, {
4432
4398
  ...getFetchConfig(),
@@ -4434,18 +4400,16 @@ async function fetchGitLabInstanceVersion() {
4434
4400
  if (!response.ok)
4435
4401
  return null;
4436
4402
  const data = await response.json();
4437
- if (typeof data === "object" &&
4438
- data !== null &&
4439
- "version" in data &&
4440
- typeof data.version === "string") {
4441
- return data.version;
4442
- }
4443
- return null;
4403
+ return parseGitLabVersionApiResponse(data);
4444
4404
  }
4445
4405
  catch {
4446
4406
  return null;
4447
4407
  }
4448
4408
  }
4409
+ async function fetchGitLabInstanceVersion() {
4410
+ const metadata = await fetchGitLabVersionMetadata();
4411
+ return metadata?.version ?? null;
4412
+ }
4449
4413
  /**
4450
4414
  * Publish all draft notes for a merge request
4451
4415
  * @param {string} projectId - The ID or URL-encoded path of the project
@@ -6453,6 +6417,262 @@ async function purgeDependencyProxyCache(groupId) {
6453
6417
  const response = await fetch(url.toString(), { ...getFetchConfig(), method: "DELETE" });
6454
6418
  await handleGitLabError(response);
6455
6419
  }
6420
+ // --- Vulnerability functions (GraphQL) ---
6421
+ //
6422
+ // The REST Vulnerabilities API is deprecated and silently ignores filter
6423
+ // params on list and reason/comment on dismiss. GitLab recommends the
6424
+ // GraphQL API for vulnerability management, which supports all of these.
6425
+ /** Convert a numeric vulnerability ID (or an existing Vulnerability GID) to a GraphQL global ID. */
6426
+ function toVulnerabilityGid(vulnerabilityId) {
6427
+ if (/^\d+$/.test(vulnerabilityId)) {
6428
+ return `gid://gitlab/Vulnerability/${vulnerabilityId}`;
6429
+ }
6430
+ if (/^gid:\/\/gitlab\/Vulnerability\/\d+$/.test(vulnerabilityId)) {
6431
+ return vulnerabilityId;
6432
+ }
6433
+ throw new Error(`Invalid vulnerability ID "${vulnerabilityId}": expected a numeric ID or a gid://gitlab/Vulnerability/<id> global ID`);
6434
+ }
6435
+ /**
6436
+ * Resolve a project ID or path to the full namespace path required by
6437
+ * GraphQL's `project(fullPath:)` query. Numeric IDs are resolved via a
6438
+ * REST lookup; paths are returned as-is (decoded).
6439
+ */
6440
+ async function resolveProjectFullPath(projectId) {
6441
+ const decoded = decodeURIComponent(getEffectiveProjectId(decodeURIComponent(projectId)));
6442
+ if (!/^\d+$/.test(decoded)) {
6443
+ return decoded;
6444
+ }
6445
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(decoded)}`);
6446
+ const response = await fetch(url.toString(), { ...getFetchConfig() });
6447
+ await handleGitLabError(response);
6448
+ const project = (await response.json());
6449
+ if (!project.path_with_namespace) {
6450
+ throw new Error(`Could not resolve full path for project ${decoded}`);
6451
+ }
6452
+ return project.path_with_namespace;
6453
+ }
6454
+ /**
6455
+ * Enforce GITLAB_ALLOWED_PROJECT_IDS for vulnerability tools. Vulnerability
6456
+ * GIDs are globally unique rather than project-scoped, so the boundary must
6457
+ * be checked against the project the vulnerability actually belongs to —
6458
+ * mirroring what getEffectiveProjectId does for project-scoped REST calls.
6459
+ * Allowlist entries may be numeric IDs or full namespace paths.
6460
+ */
6461
+ function assertVulnerabilityProjectAllowed(vulnerabilityId, project) {
6462
+ if (GITLAB_ALLOWED_PROJECT_IDS.length === 0) {
6463
+ return;
6464
+ }
6465
+ const fullPath = project?.fullPath ?? undefined;
6466
+ const numericId = project?.id?.match(/^gid:\/\/gitlab\/Project\/(\d+)$/)?.[1];
6467
+ const allowed = (fullPath !== undefined && GITLAB_ALLOWED_PROJECT_IDS.includes(fullPath)) ||
6468
+ (numericId !== undefined && GITLAB_ALLOWED_PROJECT_IDS.includes(numericId));
6469
+ if (!allowed) {
6470
+ 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(", ")}`);
6471
+ }
6472
+ }
6473
+ /**
6474
+ * Pre-flight allowlist check for vulnerability mutations: resolves the
6475
+ * vulnerability's project and verifies it against GITLAB_ALLOWED_PROJECT_IDS
6476
+ * before any write is issued. No-op (no extra request) when the allowlist
6477
+ * is not configured.
6478
+ */
6479
+ async function ensureVulnerabilityProjectAllowed(vulnerabilityId) {
6480
+ if (GITLAB_ALLOWED_PROJECT_IDS.length === 0) {
6481
+ return;
6482
+ }
6483
+ const data = await executeGraphQL(`query getVulnerabilityProject($id: VulnerabilityID!) {
6484
+ vulnerability(id: $id) {
6485
+ project {
6486
+ id
6487
+ fullPath
6488
+ }
6489
+ }
6490
+ }`, { id: toVulnerabilityGid(vulnerabilityId) });
6491
+ if (!data.vulnerability) {
6492
+ throw new Error(`Vulnerability not found: ${vulnerabilityId}`);
6493
+ }
6494
+ assertVulnerabilityProjectAllowed(vulnerabilityId, data.vulnerability.project);
6495
+ }
6496
+ /** Shared GraphQL selection set for vulnerability objects. */
6497
+ const VULNERABILITY_FIELDS = `
6498
+ id
6499
+ title
6500
+ description
6501
+ state
6502
+ severity
6503
+ reportType
6504
+ detectedAt
6505
+ confirmedAt
6506
+ resolvedAt
6507
+ dismissedAt
6508
+ dismissalReason
6509
+ webUrl
6510
+ scanner {
6511
+ name
6512
+ externalId
6513
+ vendor
6514
+ }
6515
+ identifiers {
6516
+ externalType
6517
+ externalId
6518
+ name
6519
+ url
6520
+ }
6521
+ links {
6522
+ name
6523
+ url
6524
+ }
6525
+ location {
6526
+ ... on VulnerabilityLocationSast {
6527
+ file
6528
+ startLine
6529
+ endLine
6530
+ }
6531
+ ... on VulnerabilityLocationSecretDetection {
6532
+ file
6533
+ startLine
6534
+ endLine
6535
+ }
6536
+ ... on VulnerabilityLocationDependencyScanning {
6537
+ file
6538
+ dependency {
6539
+ package {
6540
+ name
6541
+ }
6542
+ version
6543
+ }
6544
+ }
6545
+ ... on VulnerabilityLocationContainerScanning {
6546
+ image
6547
+ operatingSystem
6548
+ dependency {
6549
+ package {
6550
+ name
6551
+ }
6552
+ version
6553
+ }
6554
+ }
6555
+ ... on VulnerabilityLocationDast {
6556
+ path
6557
+ hostname
6558
+ }
6559
+ }
6560
+ `;
6561
+ async function listProjectVulnerabilities(projectId, options = {}) {
6562
+ const fullPath = await resolveProjectFullPath(projectId);
6563
+ const variables = {
6564
+ fullPath,
6565
+ first: Math.min(options.first ?? 20, 100),
6566
+ };
6567
+ if (options.state)
6568
+ variables.state = [options.state.toUpperCase()];
6569
+ if (options.severity)
6570
+ variables.severity = [options.severity.toUpperCase()];
6571
+ if (options.report_type)
6572
+ variables.reportType = [options.report_type.toUpperCase()];
6573
+ if (options.after)
6574
+ variables.after = options.after;
6575
+ const data = await executeGraphQL(`query listProjectVulnerabilities(
6576
+ $fullPath: ID!
6577
+ $state: [VulnerabilityState!]
6578
+ $severity: [VulnerabilitySeverity!]
6579
+ $reportType: [VulnerabilityReportType!]
6580
+ $first: Int
6581
+ $after: String
6582
+ ) {
6583
+ project(fullPath: $fullPath) {
6584
+ vulnerabilities(
6585
+ state: $state
6586
+ severity: $severity
6587
+ reportType: $reportType
6588
+ first: $first
6589
+ after: $after
6590
+ ) {
6591
+ nodes {
6592
+ ${VULNERABILITY_FIELDS}
6593
+ }
6594
+ pageInfo {
6595
+ endCursor
6596
+ hasNextPage
6597
+ }
6598
+ }
6599
+ }
6600
+ }`, variables);
6601
+ if (!data.project) {
6602
+ throw new Error(`Project not found or not accessible: ${fullPath}`);
6603
+ }
6604
+ return {
6605
+ vulnerabilities: data.project.vulnerabilities.nodes,
6606
+ pageInfo: data.project.vulnerabilities.pageInfo,
6607
+ };
6608
+ }
6609
+ async function getVulnerability(vulnerabilityId) {
6610
+ const data = await executeGraphQL(`query getVulnerability($id: VulnerabilityID!) {
6611
+ vulnerability(id: $id) {
6612
+ ${VULNERABILITY_FIELDS}
6613
+ project {
6614
+ id
6615
+ name
6616
+ fullPath
6617
+ }
6618
+ }
6619
+ }`, { id: toVulnerabilityGid(vulnerabilityId) });
6620
+ if (!data.vulnerability) {
6621
+ throw new Error(`Vulnerability not found: ${vulnerabilityId}`);
6622
+ }
6623
+ assertVulnerabilityProjectAllowed(vulnerabilityId, data.vulnerability.project);
6624
+ return data.vulnerability;
6625
+ }
6626
+ async function dismissVulnerability(vulnerabilityId, reason, comment) {
6627
+ await ensureVulnerabilityProjectAllowed(vulnerabilityId);
6628
+ const input = {
6629
+ id: toVulnerabilityGid(vulnerabilityId),
6630
+ dismissalReason: reason.toUpperCase(),
6631
+ };
6632
+ if (comment)
6633
+ input.comment = comment;
6634
+ const data = await executeGraphQL(`mutation dismissVulnerability($input: VulnerabilityDismissInput!) {
6635
+ vulnerabilityDismiss(input: $input) {
6636
+ vulnerability {
6637
+ id
6638
+ state
6639
+ dismissedAt
6640
+ dismissalReason
6641
+ }
6642
+ errors
6643
+ }
6644
+ }`, { input });
6645
+ if (data.vulnerabilityDismiss.errors?.length) {
6646
+ throw new Error(`Failed to dismiss vulnerability: ${data.vulnerabilityDismiss.errors.join(", ")}`);
6647
+ }
6648
+ if (!data.vulnerabilityDismiss.vulnerability) {
6649
+ throw new Error(`Vulnerability not returned after dismissal (id: ${vulnerabilityId})`);
6650
+ }
6651
+ return data.vulnerabilityDismiss.vulnerability;
6652
+ }
6653
+ async function confirmVulnerability(vulnerabilityId, comment) {
6654
+ await ensureVulnerabilityProjectAllowed(vulnerabilityId);
6655
+ const input = { id: toVulnerabilityGid(vulnerabilityId) };
6656
+ if (comment)
6657
+ input.comment = comment;
6658
+ const data = await executeGraphQL(`mutation confirmVulnerability($input: VulnerabilityConfirmInput!) {
6659
+ vulnerabilityConfirm(input: $input) {
6660
+ vulnerability {
6661
+ id
6662
+ state
6663
+ confirmedAt
6664
+ }
6665
+ errors
6666
+ }
6667
+ }`, { input });
6668
+ if (data.vulnerabilityConfirm.errors?.length) {
6669
+ throw new Error(`Failed to confirm vulnerability: ${data.vulnerabilityConfirm.errors.join(", ")}`);
6670
+ }
6671
+ if (!data.vulnerabilityConfirm.vulnerability) {
6672
+ throw new Error(`Vulnerability not returned after confirmation (id: ${vulnerabilityId})`);
6673
+ }
6674
+ return data.vulnerabilityConfirm.vulnerability;
6675
+ }
6456
6676
  /**
6457
6677
  * Upload a file to a GitLab project for use in markdown content.
6458
6678
  *
@@ -8869,6 +9089,28 @@ async function handleToolCall(params) {
8869
9089
  ],
8870
9090
  };
8871
9091
  }
9092
+ // --- Vulnerability tools ---
9093
+ case "list_project_vulnerabilities": {
9094
+ const args = ListProjectVulnerabilitiesSchema.parse(params.arguments);
9095
+ const { project_id, ...options } = args;
9096
+ const result = await listProjectVulnerabilities(project_id, options);
9097
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
9098
+ }
9099
+ case "get_vulnerability": {
9100
+ const args = GetVulnerabilitySchema.parse(params.arguments);
9101
+ const result = await getVulnerability(args.vulnerability_id);
9102
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
9103
+ }
9104
+ case "dismiss_vulnerability": {
9105
+ const args = DismissVulnerabilitySchema.parse(params.arguments);
9106
+ const result = await dismissVulnerability(args.vulnerability_id, args.reason, args.comment);
9107
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
9108
+ }
9109
+ case "confirm_vulnerability": {
9110
+ const args = ConfirmVulnerabilitySchema.parse(params.arguments);
9111
+ const result = await confirmVulnerability(args.vulnerability_id, args.comment);
9112
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
9113
+ }
8872
9114
  case "upload_markdown": {
8873
9115
  if (IS_REMOTE) {
8874
9116
  const args = MarkdownUploadRemoteSchema.parse(params.arguments);
@@ -9096,6 +9338,9 @@ async function handleToolCall(params) {
9096
9338
  const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
9097
9339
  authenticated = jobResponse.ok;
9098
9340
  }
9341
+ const versionMetadata = authenticated
9342
+ ? await fetchGitLabVersionMetadata()
9343
+ : null;
9099
9344
  return {
9100
9345
  content: [
9101
9346
  {
@@ -9104,6 +9349,7 @@ async function handleToolCall(params) {
9104
9349
  status: authenticated ? "ok" : "error",
9105
9350
  authenticated,
9106
9351
  gitlab_url: getEffectiveApiUrl(),
9352
+ ...(versionMetadata ?? {}),
9107
9353
  }),
9108
9354
  },
9109
9355
  ],
@@ -9496,8 +9742,9 @@ async function startStreamableHTTPServer() {
9496
9742
  try {
9497
9743
  apiUrl = resolveTrustedGitLabApiUrl(dynamicApiUrl);
9498
9744
  }
9499
- catch {
9500
- logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. Auth will fail.`);
9745
+ catch (error) {
9746
+ const reason = error instanceof Error ? error.message : "Invalid X-GitLab-API-URL";
9747
+ logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. ${reason}`);
9501
9748
  return null; // Reject if URL is malformed or not allowed
9502
9749
  }
9503
9750
  }
@@ -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;" };
package/build/schemas.js CHANGED
@@ -4023,3 +4023,70 @@ export const ListDependencyProxyBlobsSchema = z.object({
4023
4023
  export const PurgeDependencyProxyCacheSchema = z.object({
4024
4024
  group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
4025
4025
  });
4026
+ // --- Vulnerability schemas ---
4027
+ // Schema for listing project vulnerabilities with optional filters (GraphQL-backed)
4028
+ export const ListProjectVulnerabilitiesSchema = z.object({
4029
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
4030
+ state: z
4031
+ .enum(["detected", "confirmed", "resolved", "dismissed"])
4032
+ .optional()
4033
+ .describe("Filter by vulnerability state"),
4034
+ severity: z
4035
+ .enum(["critical", "high", "medium", "low", "info", "unknown"])
4036
+ .optional()
4037
+ .describe("Filter by severity level"),
4038
+ report_type: z
4039
+ .enum([
4040
+ "sast",
4041
+ "dast",
4042
+ "dependency_scanning",
4043
+ "container_scanning",
4044
+ "secret_detection",
4045
+ "coverage_fuzzing",
4046
+ "api_fuzzing",
4047
+ "cluster_image_scanning",
4048
+ "generic",
4049
+ ])
4050
+ .optional()
4051
+ .describe("Filter by scan/report type (e.g. secret_detection, sast, dast)"),
4052
+ first: z.coerce
4053
+ .number()
4054
+ .int()
4055
+ .min(1)
4056
+ .max(100)
4057
+ .optional()
4058
+ .describe("Number of vulnerabilities to return (max: 100, default: 20)"),
4059
+ after: z
4060
+ .string()
4061
+ .optional()
4062
+ .describe("Cursor for pagination; use the endCursor from a previous response"),
4063
+ });
4064
+ // Schema for getting a single vulnerability by ID
4065
+ export const GetVulnerabilitySchema = z.object({
4066
+ vulnerability_id: z.coerce
4067
+ .string()
4068
+ .describe("The vulnerability ID (numeric or GraphQL global ID)"),
4069
+ });
4070
+ // Schema for dismissing a vulnerability with required reason and optional comment
4071
+ export const DismissVulnerabilitySchema = z.object({
4072
+ vulnerability_id: z.coerce
4073
+ .string()
4074
+ .describe("The ID of the vulnerability to dismiss (numeric or GraphQL global ID)"),
4075
+ reason: z
4076
+ .enum([
4077
+ "acceptable_risk",
4078
+ "false_positive",
4079
+ "used_in_tests",
4080
+ "mitigating_control",
4081
+ "not_applicable",
4082
+ ])
4083
+ .describe("Reason for dismissal"),
4084
+ comment: z.string().optional().describe("Optional comment explaining the dismissal"),
4085
+ });
4086
+ // Schema for confirming a vulnerability with optional comment
4087
+ export const ConfirmVulnerabilitySchema = z.object({
4088
+ vulnerability_id: z.coerce
4089
+ .string()
4090
+ .describe("The ID of the vulnerability to confirm (numeric or GraphQL global ID)"),
4091
+ comment: z.string().optional().describe("Optional comment explaining the confirmation"),
4092
+ });
@@ -14,6 +14,7 @@ const SKILL_PATH = join(REPO_ROOT, "skills", "gitlab-mcp", "SKILL.md");
14
14
  const REF_DIR = join(REPO_ROOT, "skills", "gitlab-mcp", "reference");
15
15
  // Response field names in reference docs — not MCP tools.
16
16
  const REFERENCE_FIELD_ALLOWLIST = new Set([
17
+ "after",
17
18
  "allow_multiple",
18
19
  "deleted_file",
19
20
  "excluded_file_patterns",