@zereight/mcp-gitlab 2.1.65 → 2.1.67

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
@@ -24,6 +24,7 @@ PAT, OAuth, 읽기 전용 모드, 동적 API URL, 원격 인증을 지원하며
24
24
  - **여러 전송 방식** — 로컬 클라이언트용 stdio, 레거시 클라이언트용 SSE, 최신 원격 배포용 Streamable HTTP
25
25
  - **클라이언트 친화적 설정** — Claude Code, Codex, Antigravity, OpenCode, Copilot, Cline, Roo Code, Cursor, Kilo Code, Amp Code 예시 제공
26
26
  - **셀프 호스팅 대응** — 커스텀 GitLab 인스턴스, 프록시 설정, 동적 API URL 라우팅 지원
27
+ - **JMESPath 결과 필터링** — 도구 호출에 선택적 `jmespath` 인자(`tools/list` 참고)를 넘기면 GitLab API 요청은 그대로 두고 JSON 결과만 줄여서 반환; 응답 마스킹이 켜져 있으면 마스킹된 데이터에 JMESPath가 적용됨
27
28
 
28
29
  ### 비교 요약
29
30
 
@@ -110,7 +111,7 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
110
111
 
111
112
  예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
112
113
 
113
- 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.64`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
114
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.66`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
114
115
 
115
116
  #### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
116
117
 
package/README.md CHANGED
@@ -29,6 +29,7 @@ Supports PAT, OAuth, read-only mode, dynamic API URLs, and remote authorization
29
29
  - **Multiple transports** — stdio for local clients, SSE for legacy clients, and Streamable HTTP for modern remote deployments
30
30
  - **Client-friendly setup** — examples for Claude Code, Codex, Antigravity, OpenCode, Copilot, Cline, Roo Code, Cursor, Kilo Code, and Amp Code
31
31
  - **Self-hosted ready** — works with custom GitLab instances, proxy settings, and dynamic API URL routing
32
+ - **JMESPath result filtering** — optional `jmespath` on tool calls (see `tools/list`) shrinks JSON results without changing GitLab API requests; when response masking is enabled, JMESPath runs on masked data.
32
33
 
33
34
  ### How we compare
34
35
 
@@ -115,7 +116,7 @@ The store path is pinned by your lock file; update it with `nix flake update git
115
116
 
116
117
  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`.
117
118
 
118
- No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.64`. 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`).
119
+ No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.66`. 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`).
119
120
 
120
121
  #### Using CLI Arguments (for clients with env var issues)
121
122
 
package/README.zh-CN.md CHANGED
@@ -24,6 +24,7 @@
24
24
  - **多种传输方式** — 本地客户端使用 stdio,旧客户端使用 SSE,现代远程部署使用 Streamable HTTP
25
25
  - **客户端设置友好** — 提供 Claude Code、Codex、Antigravity、OpenCode、Copilot、Cline、Roo Code、Cursor、Kilo Code 和 Amp Code 示例
26
26
  - **适合自托管** — 支持自定义 GitLab 实例、代理设置和动态 API URL 路由
27
+ - **JMESPath 结果过滤** — 在工具调用中传入可选的 `jmespath` 参数(见 `tools/list`),可在不改变 GitLab API 请求的情况下精简 JSON 结果;启用响应掩码时,JMESPath 作用于掩码后的数据
27
28
 
28
29
  ### 对比摘要
29
30
 
@@ -110,7 +111,7 @@ command = lib.getExe inputs.gitlab-mcp.packages.${system}.default;
110
111
 
111
112
  示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
112
113
 
113
- 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.64`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
114
+ 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.66`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
114
115
 
115
116
  #### 使用 CLI 参数(适用于环境变量有问题的客户端)
116
117
 
package/build/index.js CHANGED
@@ -102,6 +102,7 @@ import { fetchWithValidatedRedirects } from "./utils/safe-redirect-fetch.js";
102
102
  import { parseGitLabVersionApiResponse, } from "./utils/gitlab-instance-version.js";
103
103
  import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
104
104
  import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
105
+ import { applyJmespathToToolResult, getJmespathSyntaxError, JMESPATH_TOOL_ARGUMENT, JMESPATH_TOOL_ARGUMENT_DESCRIPTION, omitJmespathArgument, readJmespathExpression, } from "./utils/jmespath-tool-result.js";
105
106
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
106
107
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
107
108
  import { GitLabClientPool } from "./gitlab-client-pool.js";
@@ -289,6 +290,18 @@ function createServer() {
289
290
  },
290
291
  };
291
292
  }
293
+ if (modified.inputSchema && typeof modified.inputSchema === "object") {
294
+ const properties = modified.inputSchema.properties && typeof modified.inputSchema.properties === "object"
295
+ ? { ...modified.inputSchema.properties }
296
+ : {};
297
+ if (!(JMESPATH_TOOL_ARGUMENT in properties)) {
298
+ properties[JMESPATH_TOOL_ARGUMENT] = {
299
+ type: "string",
300
+ description: JMESPATH_TOOL_ARGUMENT_DESCRIPTION,
301
+ };
302
+ modified.inputSchema = { ...modified.inputSchema, properties };
303
+ }
304
+ }
292
305
  return modified;
293
306
  });
294
307
  // <<< END: Remove $schema for Gemini compatibility >>>
@@ -303,15 +316,24 @@ function createServer() {
303
316
  const toolName = request.params.name;
304
317
  const start = Date.now();
305
318
  let maskingEngine;
306
- const logCompletion = (result) => {
319
+ const jmespathExpression = readJmespathExpression(request.params.arguments);
320
+ const logCompletion = (result, completionOptions) => {
307
321
  const durationMs = Date.now() - start;
308
322
  logger.info({ tool: toolName, event: "tool_call_done", durationMs }, `tool_call_done: ${toolName} (${durationMs}ms)`);
309
323
  const isPlainTextResult = toolName === "get_pipeline_job_output" ||
310
324
  toolName === "get_job_artifact_file" ||
311
325
  (toolName === "download_release_asset" && !IS_REMOTE);
326
+ const filtered = applyJmespathToToolResult(result, jmespathExpression, {
327
+ ...completionOptions,
328
+ maskValue: maskingEngine
329
+ ? (value) => maskingEngine.maskValue(value)
330
+ : undefined,
331
+ });
312
332
  return maskingEngine
313
- ? maskingEngine.maskToolResult(result, { textFormat: isPlainTextResult ? "plain" : "json" })
314
- : result;
333
+ ? maskingEngine.maskToolResult(filtered, {
334
+ textFormat: isPlainTextResult ? "plain" : "json",
335
+ })
336
+ : filtered;
315
337
  };
316
338
  const logError = (error) => {
317
339
  const durationMs = Date.now() - start;
@@ -370,6 +392,12 @@ function createServer() {
370
392
  maskingEngine = sessionContext
371
393
  ? sessionAuthStore.run(sessionContext, selectMaskingPolicy)
372
394
  : selectMaskingPolicy();
395
+ const jmespathSyntaxError = jmespathExpression
396
+ ? getJmespathSyntaxError(jmespathExpression)
397
+ : undefined;
398
+ if (jmespathSyntaxError) {
399
+ return logCompletion({ content: [{ type: "text", text: jmespathSyntaxError }], isError: true }, { skipJmespath: true });
400
+ }
373
401
  // Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
374
402
  if (toolName === "discover_tools") {
375
403
  const category = request.params.arguments?.category?.trim()?.toLowerCase();
@@ -477,12 +505,15 @@ function createServer() {
477
505
  text: `Tool "${toolName}" requires confirmation. This tool is marked as requiring approval before execution. Re-call with _confirmed: true to proceed.`,
478
506
  },
479
507
  ],
480
- });
508
+ }, { skipJmespath: true });
481
509
  }
482
510
  // Strip _confirmed from args before forwarding to handler
483
511
  const { _confirmed, ...cleanArgs } = request.params.arguments || {};
484
512
  request.params.arguments = cleanArgs;
485
513
  }
514
+ if (request.params.arguments && typeof request.params.arguments === "object") {
515
+ request.params.arguments = omitJmespathArgument(request.params.arguments);
516
+ }
486
517
  if (sessionContext) {
487
518
  // Policy selection and the tool handler must use the same request-local
488
519
  // API URL and project allowlist.
package/build/schemas.js CHANGED
@@ -1355,6 +1355,9 @@ export const GitLabDiffSchema = z.object({
1355
1355
  new_file: z.coerce.boolean(),
1356
1356
  renamed_file: z.coerce.boolean(),
1357
1357
  deleted_file: z.coerce.boolean(),
1358
+ generated_file: z.coerce.boolean().optional(),
1359
+ too_large: z.coerce.boolean().optional(),
1360
+ collapsed: z.coerce.boolean().optional(),
1358
1361
  });
1359
1362
  // Response schemas for operations
1360
1363
  export const GitLabCreateUpdateFileResponseSchema = z.object({
@@ -0,0 +1,36 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { GitLabDiffSchema } from "../schemas.js";
4
+ test("diff schema keeps GitLab's generated_file, too_large and collapsed flags", () => {
5
+ const parsed = GitLabDiffSchema.parse({
6
+ diff: "@@ -1 +1 @@",
7
+ new_path: "src/Foo.php",
8
+ old_path: "src/Foo.php",
9
+ a_mode: "100644",
10
+ b_mode: "100644",
11
+ new_file: false,
12
+ renamed_file: false,
13
+ deleted_file: false,
14
+ generated_file: true,
15
+ too_large: true,
16
+ collapsed: false,
17
+ });
18
+ assert.equal(parsed.generated_file, true);
19
+ assert.equal(parsed.too_large, true);
20
+ assert.equal(parsed.collapsed, false);
21
+ });
22
+ test("diff schema still parses entries without the optional flags", () => {
23
+ const parsed = GitLabDiffSchema.parse({
24
+ diff: "@@ -1 +1 @@",
25
+ new_path: "src/Foo.php",
26
+ old_path: "src/Foo.php",
27
+ a_mode: "100644",
28
+ b_mode: "100644",
29
+ new_file: false,
30
+ renamed_file: false,
31
+ deleted_file: false,
32
+ });
33
+ assert.equal("generated_file" in parsed, false);
34
+ assert.equal("too_large" in parsed, false);
35
+ assert.equal("collapsed" in parsed, false);
36
+ });
@@ -188,6 +188,39 @@ describe("managed response masking at the MCP boundary", { timeout: 30_000 }, ()
188
188
  "/api/v4/projects/12/merge_requests",
189
189
  ]);
190
190
  });
191
+ test("applies jmespath to JSON tool results at the MCP boundary", async (t) => {
192
+ const { client } = await startMaskingServer(t, {
193
+ env: { ENABLE_STRICT_PROJECT_SCOPE: "true", GITLAB_ALLOWED_PROJECT_IDS: "12" },
194
+ });
195
+ const { tools } = await client.listTools();
196
+ const listMrs = tools.find(tool => tool.name === "list_merge_requests");
197
+ const jmespathSchema = listMrs?.inputSchema?.properties?.jmespath;
198
+ assert.ok(jmespathSchema, "tools/list should inject optional jmespath on every tool schema");
199
+ assert.ok((jmespathSchema.description?.length ?? 0) < 120, "jmespath schema description should stay short for tools/list token cost");
200
+ const result = await client.callTool({
201
+ name: "list_issues",
202
+ arguments: { jmespath: "[].title" },
203
+ });
204
+ const text = result.content[0]?.text;
205
+ assert.deepEqual(JSON.parse(text), ["[one]"]);
206
+ const probe = await client.callTool({
207
+ name: "list_issues",
208
+ arguments: { jmespath: "[].starts_with(title, `ProjectOne`)" },
209
+ });
210
+ const probeText = probe.content[0]?.text;
211
+ assert.deepEqual(JSON.parse(probeText), [false], "jmespath must run on masked JSON so callers cannot probe hidden substrings");
212
+ });
213
+ test("rejects invalid jmespath before the handler reaches GitLab", async (t) => {
214
+ const { client, requests } = await startMaskingServer(t, {
215
+ env: { ENABLE_STRICT_PROJECT_SCOPE: "true", GITLAB_ALLOWED_PROJECT_IDS: "12" },
216
+ });
217
+ const result = await client.callTool({
218
+ name: "list_issues",
219
+ arguments: { jmespath: "[[[" },
220
+ });
221
+ assert.equal(result.isError, true);
222
+ assert.deepEqual(requests, []);
223
+ });
191
224
  test("keeps optional default-project handlers available with their managed policy", async (t) => {
192
225
  const { client, requests } = await startMaskingServer(t);
193
226
  const fileResult = await client.callTool({
@@ -0,0 +1,91 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, test } from "node:test";
3
+ import { applyJmespathFilter, applyJmespathToToolResult, getJmespathSyntaxError, omitJmespathArgument, readJmespathExpression, shouldApplyJmespathFilter, } from "../../utils/jmespath-tool-result.js";
4
+ describe("jmespath tool result helpers", () => {
5
+ test("readJmespathExpression trims and rejects blank values", () => {
6
+ assert.equal(readJmespathExpression({ jmespath: " [].title " }), "[].title");
7
+ assert.equal(readJmespathExpression({ jmespath: " " }), undefined);
8
+ assert.equal(readJmespathExpression({ jmespath: 1 }), undefined);
9
+ });
10
+ test("omitJmespathArgument removes MCP-only key", () => {
11
+ assert.deepEqual(omitJmespathArgument({ project_id: "1", jmespath: "[].id" }), {
12
+ project_id: "1",
13
+ });
14
+ });
15
+ test("applyJmespathFilter projects array fields", () => {
16
+ const data = [
17
+ { title: "a", description: "long" },
18
+ { title: "b", description: "longer" },
19
+ ];
20
+ assert.deepEqual(applyJmespathFilter(data, "[].title"), ["a", "b"]);
21
+ });
22
+ test("applyJmespathToToolResult filters JSON text content", () => {
23
+ const result = applyJmespathToToolResult({
24
+ content: [
25
+ {
26
+ type: "text",
27
+ text: JSON.stringify([{ title: "mr-one", iid: 1 }]),
28
+ },
29
+ ],
30
+ }, "[].title");
31
+ assert.equal(result.content[0].text, JSON.stringify(["mr-one"]));
32
+ });
33
+ test("applyJmespathToToolResult applies maskValue before JMESPath", () => {
34
+ const result = applyJmespathToToolResult({
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: JSON.stringify([{ title: "ProjectOneSecret" }]),
39
+ },
40
+ ],
41
+ }, "[].starts_with(title, `ProjectOne`)", {
42
+ maskValue: value => {
43
+ if (!Array.isArray(value))
44
+ return value;
45
+ return value.map(entry => entry && typeof entry === "object" && "title" in entry
46
+ ? { ...entry, title: "[one]" }
47
+ : entry);
48
+ },
49
+ });
50
+ assert.deepEqual(JSON.parse(result.content[0].text), [false]);
51
+ });
52
+ test("shouldApplyJmespathFilter is false for isError and explicit skip", () => {
53
+ const errorResult = {
54
+ content: [{ type: "text", text: 'Unknown category "foo"' }],
55
+ isError: true,
56
+ };
57
+ assert.equal(shouldApplyJmespathFilter(errorResult, "[].title"), false);
58
+ assert.equal(shouldApplyJmespathFilter({ content: [{ type: "text", text: "requires confirmation" }] }, "[].title", { skipJmespath: true }), false);
59
+ assert.equal(shouldApplyJmespathFilter({ content: [{ type: "text", text: "[]" }] }, "[].title"), true);
60
+ });
61
+ test("applyJmespathToToolResult preserves isError tool results", () => {
62
+ const original = {
63
+ content: [{ type: "text", text: 'Unknown category "foo"' }],
64
+ isError: true,
65
+ };
66
+ assert.deepEqual(applyJmespathToToolResult(original, "[].title"), original);
67
+ });
68
+ test("applyJmespathToToolResult preserves approval-style plain text when skipJmespath", () => {
69
+ const original = {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: 'Tool "create_issue" requires confirmation. Re-call with _confirmed: true to proceed.',
74
+ },
75
+ ],
76
+ };
77
+ assert.deepEqual(applyJmespathToToolResult(original, "[].title", { skipJmespath: true }), original);
78
+ });
79
+ test("applyJmespathToToolResult passes non-JSON success text through unchanged", () => {
80
+ const original = { content: [{ type: "text", text: "Created pipeline #1 for main." }] };
81
+ assert.deepEqual(applyJmespathToToolResult(original, "id"), original);
82
+ });
83
+ test("applyJmespathToToolResult returns the original result when evaluation fails", () => {
84
+ const original = { content: [{ type: "text", text: JSON.stringify({ title: 1 }) }] };
85
+ assert.deepEqual(applyJmespathToToolResult(original, "starts_with(title, `a`)"), original);
86
+ });
87
+ test("getJmespathSyntaxError rejects invalid expressions before execution", () => {
88
+ assert.match(getJmespathSyntaxError("[[[") ?? "", /Invalid jmespath expression/);
89
+ assert.equal(getJmespathSyntaxError("[].title"), undefined);
90
+ });
91
+ });
@@ -0,0 +1,78 @@
1
+ import jmespath from "jmespath";
2
+ /** MCP-only argument; stripped before GitLab handlers run (issue #766). */
3
+ export const JMESPATH_TOOL_ARGUMENT = "jmespath";
4
+ export const JMESPATH_TOOL_ARGUMENT_DESCRIPTION = "Optional JMESPath expression filtering the JSON result before return.";
5
+ export function readJmespathExpression(args) {
6
+ if (!args)
7
+ return undefined;
8
+ const raw = args[JMESPATH_TOOL_ARGUMENT];
9
+ if (typeof raw !== "string")
10
+ return undefined;
11
+ const trimmed = raw.trim();
12
+ return trimmed.length > 0 ? trimmed : undefined;
13
+ }
14
+ /** Syntax check before the handler runs, so side-effecting tools never execute with a bad expression. */
15
+ export function getJmespathSyntaxError(expression) {
16
+ try {
17
+ jmespath.compile(expression);
18
+ return undefined;
19
+ }
20
+ catch (error) {
21
+ return `Invalid jmespath expression: ${error instanceof Error ? error.message : String(error)}`;
22
+ }
23
+ }
24
+ export function omitJmespathArgument(args) {
25
+ if (!(JMESPATH_TOOL_ARGUMENT in args))
26
+ return args;
27
+ const { [JMESPATH_TOOL_ARGUMENT]: _omit, ...rest } = args;
28
+ return rest;
29
+ }
30
+ export function applyJmespathFilter(data, expression) {
31
+ return jmespath.search(data, expression);
32
+ }
33
+ function isContentBlock(value) {
34
+ return Boolean(value && typeof value === "object");
35
+ }
36
+ function prepareDataForJmespath(data, maskValue) {
37
+ return maskValue ? maskValue(data) : data;
38
+ }
39
+ function parseJson(text) {
40
+ try {
41
+ return { ok: true, value: JSON.parse(text) };
42
+ }
43
+ catch {
44
+ return { ok: false };
45
+ }
46
+ }
47
+ /** When false, return the tool result unchanged (masking still runs afterward). */
48
+ export function shouldApplyJmespathFilter(result, expression, options) {
49
+ return Boolean(expression && !options?.skipJmespath && !result.isError);
50
+ }
51
+ /**
52
+ * Apply JMESPath to JSON serialized in text content blocks (and structuredContent when present).
53
+ * The handler has already run, so this never turns a result into an error: non-JSON text and
54
+ * evaluation failures return the original result.
55
+ */
56
+ export function applyJmespathToToolResult(result, expression, options) {
57
+ if (!expression || !shouldApplyJmespathFilter(result, expression, options))
58
+ return result;
59
+ const maskValue = options?.maskValue;
60
+ const filter = (data) => applyJmespathFilter(prepareDataForJmespath(data, maskValue), expression);
61
+ try {
62
+ const content = Array.isArray(result.content)
63
+ ? result.content.map(block => {
64
+ if (!isContentBlock(block) || block.type !== "text" || typeof block.text !== "string") {
65
+ return block;
66
+ }
67
+ const parsed = parseJson(block.text);
68
+ return parsed.ok ? { ...block, text: JSON.stringify(filter(parsed.value)) } : block;
69
+ })
70
+ : result.content;
71
+ return result.structuredContent === undefined
72
+ ? { ...result, content }
73
+ : { ...result, content, structuredContent: filter(result.structuredContent) };
74
+ }
75
+ catch {
76
+ return result;
77
+ }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.1.65",
3
+ "version": "2.1.67",
4
4
  "mcpName": "io.github.zereight/gitlab-mcp",
5
5
  "description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
6
6
  "keywords": [
@@ -78,6 +78,7 @@
78
78
  "express": "^5.1.0",
79
79
  "express-rate-limit": "^8.5.2",
80
80
  "fetch-cookie": "^3.1.0",
81
+ "jmespath": "^0.16.0",
81
82
  "open": "^10.2.0",
82
83
  "pino": "^9.7.0",
83
84
  "pino-pretty": "^13.0.0",
@@ -90,6 +91,7 @@
90
91
  },
91
92
  "devDependencies": {
92
93
  "@types/express": "^5.0.2",
94
+ "@types/jmespath": "^0.15.2",
93
95
  "@types/node": "^22.13.10",
94
96
  "@typescript-eslint/eslint-plugin": "^8.21.0",
95
97
  "@typescript-eslint/parser": "^8.21.0",