@zereight/mcp-gitlab 2.1.25 → 2.1.27

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.
@@ -0,0 +1,177 @@
1
+ import { after, before, describe, test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { spawn } from "child_process";
4
+ import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
5
+ const MOCK_TOKEN = "mock-ci-catalog-token";
6
+ async function callTool(toolName, args, env) {
7
+ return new Promise((resolve, reject) => {
8
+ const proc = spawn("node", ["build/index.js"], {
9
+ stdio: ["pipe", "pipe", "pipe"],
10
+ env: {
11
+ ...process.env,
12
+ ...env,
13
+ },
14
+ });
15
+ proc.on("error", reject);
16
+ let output = "";
17
+ let errorOutput = "";
18
+ proc.stdout?.on("data", (d) => (output += d));
19
+ proc.stderr?.on("data", (d) => (errorOutput += d));
20
+ proc.on("close", code => {
21
+ if (code !== 0) {
22
+ reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
23
+ return;
24
+ }
25
+ try {
26
+ const line = output.split("\n").find(l => l.startsWith("{"));
27
+ if (!line) {
28
+ reject(new Error("No JSON output found"));
29
+ return;
30
+ }
31
+ const response = JSON.parse(line);
32
+ if (response.error) {
33
+ reject(new Error(response.error.message ?? JSON.stringify(response.error)));
34
+ return;
35
+ }
36
+ const content = response.result?.content?.[0]?.text;
37
+ resolve(content ? JSON.parse(content) : response.result);
38
+ }
39
+ catch (error) {
40
+ reject(error);
41
+ }
42
+ });
43
+ proc.stdin?.end(JSON.stringify({
44
+ jsonrpc: "2.0",
45
+ id: 1,
46
+ method: "tools/call",
47
+ params: { name: toolName, arguments: args },
48
+ }) + "\n");
49
+ });
50
+ }
51
+ describe("CI/CD Catalog tools", () => {
52
+ let mockGitLab;
53
+ let mockGitLabUrl;
54
+ const env = () => ({
55
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
56
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
57
+ });
58
+ before(async () => {
59
+ const port = await findMockServerPort(9000);
60
+ mockGitLab = new MockGitLabServer({ port, validTokens: [MOCK_TOKEN] });
61
+ mockGitLab.addRootHandler("post", "/api/graphql", (req, res) => {
62
+ if (req.body.query.includes("ListCiCatalogResources")) {
63
+ assert.strictEqual(req.body.variables.search, "docker");
64
+ res.json({
65
+ data: {
66
+ ciCatalogResources: {
67
+ nodes: [
68
+ {
69
+ id: "gid://gitlab/Ci::Catalog::Resource/1",
70
+ name: "Docker",
71
+ description: "Build Docker images",
72
+ fullPath: "components/docker",
73
+ icon: null,
74
+ starCount: 3,
75
+ topics: ["ci"],
76
+ verificationLevel: "UNVERIFIED",
77
+ visibilityLevel: "public",
78
+ webPath: "/components/docker",
79
+ latestReleasedAt: "2026-01-01T00:00:00Z",
80
+ last30DayUsageCount: 7,
81
+ },
82
+ ],
83
+ pageInfo: { hasNextPage: false, endCursor: null },
84
+ },
85
+ },
86
+ });
87
+ return;
88
+ }
89
+ res.json({
90
+ data: {
91
+ ciCatalogResource: {
92
+ id: "gid://gitlab/Ci::Catalog::Resource/1",
93
+ name: "Docker",
94
+ description: "Build Docker images",
95
+ fullPath: "components/docker",
96
+ icon: null,
97
+ starCount: 3,
98
+ topics: ["ci"],
99
+ verificationLevel: "UNVERIFIED",
100
+ visibilityLevel: "public",
101
+ webPath: "/components/docker",
102
+ latestReleasedAt: "2026-01-01T00:00:00Z",
103
+ last30DayUsageCount: 7,
104
+ versions: {
105
+ nodes: [
106
+ {
107
+ id: "gid://gitlab/Ci::Catalog::Resources::Version/1",
108
+ name: "1.0.0",
109
+ path: "/components/docker/-/releases/1.0.0",
110
+ createdAt: "2026-01-01T00:00:00Z",
111
+ releasedAt: "2026-01-01T00:00:00Z",
112
+ semver: { major: 1, minor: 0, patch: 0 },
113
+ components: {
114
+ nodes: [
115
+ {
116
+ id: "gid://gitlab/Ci::Catalog::Resources::Component/1",
117
+ name: "build",
118
+ description: "Build an image",
119
+ includePath: "components/docker/build@1.0.0",
120
+ last30DayUsageCount: 4,
121
+ inputs: [
122
+ {
123
+ name: "image",
124
+ description: "Image name",
125
+ type: "STRING",
126
+ required: true,
127
+ default: null,
128
+ options: null,
129
+ regex: null,
130
+ },
131
+ ],
132
+ },
133
+ {
134
+ id: "gid://gitlab/Ci::Catalog::Resources::Component/2",
135
+ name: "scan",
136
+ description: "Scan an image",
137
+ includePath: "components/docker/scan@1.0.0",
138
+ last30DayUsageCount: 2,
139
+ inputs: [],
140
+ },
141
+ ],
142
+ pageInfo: { hasNextPage: false, endCursor: null },
143
+ },
144
+ },
145
+ ],
146
+ pageInfo: { hasNextPage: false, endCursor: null },
147
+ },
148
+ },
149
+ },
150
+ });
151
+ });
152
+ await mockGitLab.start();
153
+ mockGitLabUrl = mockGitLab.getUrl();
154
+ });
155
+ after(async () => {
156
+ await mockGitLab.stop();
157
+ });
158
+ test("list_ci_catalog_resources calls GitLab GraphQL", async () => {
159
+ const result = await callTool("list_ci_catalog_resources", { search: "docker" }, env());
160
+ const resource = result.data.ciCatalogResources.nodes[0];
161
+ assert.strictEqual(resource.name, "Docker");
162
+ assert.strictEqual(resource.fullPath, "components/docker");
163
+ });
164
+ test("get_ci_catalog_resource can filter returned components by name", async () => {
165
+ const result = await callTool("get_ci_catalog_resource", { full_path: "components/docker", component_name: "build" }, env());
166
+ const components = result.data.ciCatalogResource.versions.nodes[0].components.nodes;
167
+ assert.strictEqual(components.length, 1);
168
+ assert.strictEqual(components[0].name, "build");
169
+ assert.strictEqual(components[0].inputs[0].name, "image");
170
+ });
171
+ test("get_ci_catalog_resource rejects empty identity values", async () => {
172
+ await assert.rejects(() => callTool("get_ci_catalog_resource", { full_path: "" }, env()), /full_path|Too small|minLength|invalid/i);
173
+ });
174
+ test("get_ci_catalog_resource rejects multiple identity values", async () => {
175
+ await assert.rejects(() => callTool("get_ci_catalog_resource", { id: "gid://gitlab/Ci::Catalog::Resource/1", full_path: "components/docker" }, env()), /exactly one|id|full_path/i);
176
+ });
177
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { spawn } from "child_process";
4
+ import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
5
+ const MOCK_TOKEN = "mock-token-create-repository";
6
+ const TEST_NAMESPACE_ID = 6;
7
+ const MOCK_CREATED_PROJECT = {
8
+ id: "99",
9
+ name: "my-docs",
10
+ path_with_namespace: "my-group/my-docs",
11
+ description: null,
12
+ visibility: "private",
13
+ };
14
+ async function callCreateRepository(args, env) {
15
+ return new Promise((resolve, reject) => {
16
+ const proc = spawn("node", ["build/index.js"], {
17
+ stdio: ["pipe", "pipe", "pipe"],
18
+ env: { ...process.env, ...env },
19
+ });
20
+ let output = "";
21
+ let errorOutput = "";
22
+ proc.stdout?.on("data", (d) => (output += d));
23
+ proc.stderr?.on("data", (d) => (errorOutput += d));
24
+ proc.on("close", code => {
25
+ if (code !== 0) {
26
+ return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
27
+ }
28
+ const line = output.split("\n").find(l => l.startsWith("{"));
29
+ if (!line)
30
+ return reject(new Error("No JSON output found"));
31
+ try {
32
+ const response = JSON.parse(line);
33
+ if (response.error) {
34
+ reject(new Error(response.error?.message ?? String(response.error)));
35
+ }
36
+ else {
37
+ const content = response.result?.content?.[0]?.text;
38
+ resolve(content ? JSON.parse(content) : response.result);
39
+ }
40
+ }
41
+ catch (e) {
42
+ reject(e);
43
+ }
44
+ });
45
+ proc.stdin?.end(JSON.stringify({
46
+ jsonrpc: "2.0",
47
+ id: 1,
48
+ method: "tools/call",
49
+ params: { name: "create_repository", arguments: args },
50
+ }) + "\n");
51
+ });
52
+ }
53
+ describe("When create_repository is called", () => {
54
+ describe("with namespace_id", () => {
55
+ test("should forward namespace_id in POST /projects body", async () => {
56
+ const mockPort = await findMockServerPort();
57
+ const mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN] });
58
+ let receivedBody;
59
+ mockServer.addMockHandler("post", "/projects", (req, res) => {
60
+ receivedBody = req.body;
61
+ res.status(201).json(MOCK_CREATED_PROJECT);
62
+ });
63
+ await mockServer.start();
64
+ try {
65
+ const result = await callCreateRepository({ name: "my-docs", namespace_id: TEST_NAMESPACE_ID }, {
66
+ GITLAB_API_URL: `${mockServer.getUrl()}/api/v4`,
67
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
68
+ });
69
+ assert.deepStrictEqual(receivedBody?.namespace_id, TEST_NAMESPACE_ID);
70
+ assert.strictEqual(result.name, "my-docs");
71
+ }
72
+ finally {
73
+ await mockServer.stop();
74
+ }
75
+ });
76
+ test("should accept string namespace_id from list_namespaces", async () => {
77
+ const mockPort = await findMockServerPort();
78
+ const mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN] });
79
+ let receivedBody;
80
+ mockServer.addMockHandler("post", "/projects", (req, res) => {
81
+ receivedBody = req.body;
82
+ res.status(201).json(MOCK_CREATED_PROJECT);
83
+ });
84
+ await mockServer.start();
85
+ try {
86
+ await callCreateRepository({ name: "my-docs", namespace_id: String(TEST_NAMESPACE_ID) }, {
87
+ GITLAB_API_URL: `${mockServer.getUrl()}/api/v4`,
88
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
89
+ });
90
+ assert.deepStrictEqual(receivedBody?.namespace_id, TEST_NAMESPACE_ID);
91
+ }
92
+ finally {
93
+ await mockServer.stop();
94
+ }
95
+ });
96
+ });
97
+ describe("without namespace_id", () => {
98
+ test("should omit namespace_id from POST /projects body", async () => {
99
+ const mockPort = await findMockServerPort();
100
+ const mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN] });
101
+ let receivedBody;
102
+ mockServer.addMockHandler("post", "/projects", (req, res) => {
103
+ receivedBody = req.body;
104
+ res.status(201).json(MOCK_CREATED_PROJECT);
105
+ });
106
+ await mockServer.start();
107
+ try {
108
+ await callCreateRepository({ name: "my-docs" }, {
109
+ GITLAB_API_URL: `${mockServer.getUrl()}/api/v4`,
110
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
111
+ });
112
+ assert.ok(receivedBody);
113
+ assert.ok(!Object.hasOwn(receivedBody, "namespace_id"));
114
+ }
115
+ finally {
116
+ await mockServer.stop();
117
+ }
118
+ });
119
+ });
120
+ });
@@ -4,7 +4,7 @@ import { spawn } from "child_process";
4
4
  import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
5
5
  const MOCK_TOKEN = "glpat-mock-token-12345";
6
6
  const TEST_PROJECT_ID = "123";
7
- async function callListIssues(args = {}, env) {
7
+ async function callListIssuesResult(args = {}, env) {
8
8
  return new Promise((resolve, reject) => {
9
9
  const proc = spawn("node", ["build/index.js"], {
10
10
  stdio: ["pipe", "pipe", "pipe"],
@@ -35,14 +35,14 @@ async function callListIssues(args = {}, env) {
35
35
  const content = response.result?.content?.[0]?.text;
36
36
  if (content) {
37
37
  try {
38
- resolve(JSON.parse(content));
38
+ resolve({ data: JSON.parse(content), text: content });
39
39
  }
40
40
  catch {
41
41
  reject(new Error(`Failed to parse tool output JSON: ${content}`));
42
42
  }
43
43
  }
44
44
  else {
45
- resolve(response.result);
45
+ resolve({ data: response.result });
46
46
  }
47
47
  }
48
48
  }
@@ -58,6 +58,9 @@ async function callListIssues(args = {}, env) {
58
58
  }) + "\n");
59
59
  });
60
60
  }
61
+ async function callListIssues(args = {}, env) {
62
+ return (await callListIssuesResult(args, env)).data;
63
+ }
61
64
  describe("list_issues", () => {
62
65
  let mockGitLab;
63
66
  let mockGitLabUrl;
@@ -85,6 +88,15 @@ describe("list_issues", () => {
85
88
  const title = Reflect.get(firstIssue, "title");
86
89
  assert.strictEqual(title, "Test Issue 1");
87
90
  });
91
+ test("returns compact JSON text", async () => {
92
+ const result = await callListIssuesResult({ project_id: TEST_PROJECT_ID }, {
93
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
94
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
95
+ });
96
+ assert.ok(result.text, "Tool response should include text content");
97
+ assert.doesNotMatch(result.text, /\n\s+"/, "Tool response JSON should not be pretty-printed");
98
+ assert.ok(Array.isArray(result.data), "Response should remain valid JSON");
99
+ });
88
100
  test("prefers author_username over author_id when both are provided", async () => {
89
101
  let capturedUrl;
90
102
  mockGitLab.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/issues`, (req, res) => {
@@ -20,9 +20,9 @@ const TOOLSET_TOOL_COUNTS = {
20
20
  issues: 24,
21
21
  repositories: 7,
22
22
  branches: 15,
23
- projects: 9,
23
+ projects: 10,
24
24
  labels: 5,
25
- ci: 2,
25
+ ci: 4,
26
26
  pipelines: 19,
27
27
  milestones: 9,
28
28
  wiki: 10,
@@ -36,7 +36,8 @@ const TOOLSET_TOOL_COUNTS = {
36
36
  variables: 10,
37
37
  dependency_proxy: 4,
38
38
  };
39
- const LEGACY_PIPELINE_TOOL_COUNT = TOOLSET_TOOL_COUNTS.pipelines + TOOLSET_TOOL_COUNTS.ci;
39
+ const LEGACY_PIPELINE_CI_TOOL_COUNT = 2;
40
+ const LEGACY_PIPELINE_TOOL_COUNT = TOOLSET_TOOL_COUNTS.pipelines + LEGACY_PIPELINE_CI_TOOL_COUNT;
40
41
  const DEFAULT_TOOLSETS = [
41
42
  "merge_requests",
42
43
  "issues",
@@ -70,9 +71,9 @@ const TOOLSET_SAMPLE_TOOLS = {
70
71
  issues: ["create_issue", "list_issues", "create_note", "list_todos"],
71
72
  repositories: ["search_repositories", "get_file_contents", "push_files"],
72
73
  branches: ["create_branch", "get_branch", "list_branches", "delete_branch", "list_commits", "list_commit_statuses", "create_commit_status"],
73
- projects: ["get_project", "list_namespaces", "list_group_iterations"],
74
+ projects: ["get_project", "update_project", "list_namespaces", "list_group_iterations"],
74
75
  labels: ["list_labels", "create_label"],
75
- ci: ["validate_ci_lint", "validate_project_ci_lint"],
76
+ ci: ["validate_ci_lint", "validate_project_ci_lint", "list_ci_catalog_resources", "get_ci_catalog_resource"],
76
77
  pipelines: ["list_pipelines", "create_pipeline", "cancel_pipeline_job", "list_deployments", "list_job_artifacts"],
77
78
  milestones: ["list_milestones", "create_milestone", "get_milestone_burndown_events"],
78
79
  wiki: ["list_wiki_pages", "create_wiki_page", "list_group_wiki_pages", "create_group_wiki_page"],
@@ -0,0 +1,112 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { spawn } from "child_process";
4
+ import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
5
+ const MOCK_TOKEN = "mock-update-project-token";
6
+ const TEST_PROJECT_ID = "123";
7
+ async function callUpdateProject(args, env) {
8
+ return new Promise((resolve, reject) => {
9
+ const proc = spawn("node", ["build/index.js"], {
10
+ stdio: ["pipe", "pipe", "pipe"],
11
+ env: { ...process.env, ...env },
12
+ });
13
+ let output = "";
14
+ let errorOutput = "";
15
+ proc.stdout?.on("data", (d) => (output += d));
16
+ proc.stderr?.on("data", (d) => (errorOutput += d));
17
+ proc.on("close", code => {
18
+ if (code !== 0) {
19
+ reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
20
+ return;
21
+ }
22
+ const line = output.split("\n").find(l => l.startsWith("{"));
23
+ if (!line) {
24
+ reject(new Error("No JSON output found"));
25
+ return;
26
+ }
27
+ const response = JSON.parse(line);
28
+ if (response.error) {
29
+ reject(new Error(response.error?.message ?? String(response.error)));
30
+ return;
31
+ }
32
+ const content = response.result?.content?.[0]?.text;
33
+ resolve(content ? JSON.parse(content) : response.result);
34
+ });
35
+ proc.stdin?.end(JSON.stringify({
36
+ jsonrpc: "2.0",
37
+ id: 1,
38
+ method: "tools/call",
39
+ params: { name: "update_project", arguments: args },
40
+ }) + "\n");
41
+ });
42
+ }
43
+ describe("update_project", () => {
44
+ test("sends project settings to PUT /projects/:id", async () => {
45
+ const mockPort = await findMockServerPort();
46
+ const mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN] });
47
+ let receivedBody;
48
+ mockServer.addMockHandler("put", `/projects/${TEST_PROJECT_ID}`, (req, res) => {
49
+ receivedBody = req.body;
50
+ res.json({ id: Number(TEST_PROJECT_ID), ...receivedBody });
51
+ });
52
+ await mockServer.start();
53
+ try {
54
+ const result = await callUpdateProject({
55
+ project_id: TEST_PROJECT_ID,
56
+ description: "Managed by MCP",
57
+ visibility: "private",
58
+ topics: ["ai", "mcp"],
59
+ issues_access_level: "enabled",
60
+ wiki_access_level: "disabled",
61
+ remove_source_branch_after_merge: "true",
62
+ }, {
63
+ GITLAB_API_URL: `${mockServer.getUrl()}/api/v4`,
64
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
65
+ });
66
+ assert.deepStrictEqual(receivedBody, {
67
+ description: "Managed by MCP",
68
+ visibility: "private",
69
+ topics: ["ai", "mcp"],
70
+ issues_access_level: "enabled",
71
+ wiki_access_level: "disabled",
72
+ remove_source_branch_after_merge: true,
73
+ });
74
+ assert.strictEqual(result.description, "Managed by MCP");
75
+ }
76
+ finally {
77
+ await mockServer.stop();
78
+ }
79
+ });
80
+ test("keeps string false as boolean false", async () => {
81
+ const mockPort = await findMockServerPort();
82
+ const mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN] });
83
+ let receivedBody;
84
+ mockServer.addMockHandler("put", `/projects/${TEST_PROJECT_ID}`, (req, res) => {
85
+ receivedBody = req.body;
86
+ res.json({ id: Number(TEST_PROJECT_ID), ...receivedBody });
87
+ });
88
+ await mockServer.start();
89
+ try {
90
+ await callUpdateProject({ project_id: TEST_PROJECT_ID, remove_source_branch_after_merge: "false" }, {
91
+ GITLAB_API_URL: `${mockServer.getUrl()}/api/v4`,
92
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
93
+ });
94
+ assert.strictEqual(receivedBody?.remove_source_branch_after_merge, false);
95
+ }
96
+ finally {
97
+ await mockServer.stop();
98
+ }
99
+ });
100
+ test("rejects public access level for non-pages features", async () => {
101
+ await assert.rejects(() => callUpdateProject({ project_id: TEST_PROJECT_ID, issues_access_level: "public" }, {
102
+ GITLAB_API_URL: "https://gitlab.example.com/api/v4",
103
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
104
+ }), /Invalid enum value/);
105
+ });
106
+ test("rejects empty updates before calling GitLab", async () => {
107
+ await assert.rejects(() => callUpdateProject({ project_id: TEST_PROJECT_ID }, {
108
+ GITLAB_API_URL: "https://gitlab.example.com/api/v4",
109
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
110
+ }), /Provide at least one project setting/);
111
+ });
112
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { getForwardedPublicBaseUrl } from "../../utils/forwarded-public-base-url.js";
4
+ function req(headers) {
5
+ return { headers };
6
+ }
7
+ describe("getForwardedPublicBaseUrl", () => {
8
+ test("returns undefined unless proxy headers are trusted", () => {
9
+ assert.strictEqual(getForwardedPublicBaseUrl(req({ "x-forwarded-proto": "https", "x-forwarded-host": "gitlab.example.com" }), false), undefined);
10
+ });
11
+ test("builds a public base URL from forwarded headers", () => {
12
+ assert.strictEqual(getForwardedPublicBaseUrl(req({
13
+ "x-forwarded-proto": "https",
14
+ "x-forwarded-host": "mcp.example.com",
15
+ "x-forwarded-prefix": "/gitlab-mcp/",
16
+ }), true), "https://mcp.example.com/gitlab-mcp");
17
+ });
18
+ test("uses the last comma-separated forwarded value", () => {
19
+ assert.strictEqual(getForwardedPublicBaseUrl(req({
20
+ "x-forwarded-proto": "http, https",
21
+ "x-forwarded-host": "internal, mcp.example.com",
22
+ }), true), "https://mcp.example.com");
23
+ });
24
+ test("parses quoted Forwarded header values", () => {
25
+ assert.strictEqual(getForwardedPublicBaseUrl(req({
26
+ forwarded: 'for=192.0.2.43; proto="https"; host="mcp.example.com"',
27
+ "x-forwarded-prefix": "/gitlab-mcp",
28
+ }), true), "https://mcp.example.com/gitlab-mcp");
29
+ });
30
+ test("rejects unsafe host and prefix values", () => {
31
+ assert.strictEqual(getForwardedPublicBaseUrl(req({ "x-forwarded-proto": "https", "x-forwarded-host": "bad/host" }), true), undefined);
32
+ assert.strictEqual(getForwardedPublicBaseUrl(req({
33
+ "x-forwarded-proto": "https",
34
+ "x-forwarded-host": "mcp.example.com",
35
+ "x-forwarded-prefix": "//evil",
36
+ }), true), "https://mcp.example.com");
37
+ });
38
+ });
@@ -1,7 +1,7 @@
1
1
  import { zodToJsonSchema } from "zod-to-json-schema";
2
2
  import { toJSONSchema } from "../utils/schema.js";
3
3
  import { USE_GITLAB_WIKI, USE_MILESTONE, USE_PIPELINE, SSE, STREAMABLE_HTTP, } from "../config.js";
4
- import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadAttachmentRemoteSchema, DownloadJobArtifactsSchema, DownloadJobArtifactsRemoteSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, GetBranchSchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GetDeploymentSchema, GetDraftNoteSchema, GetEnvironmentSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetTimelineEventsSchema, GetUsersSchema, GetUserSchema, WhoAmISchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListBranchesSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListPipelinesSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListReleasesSchema, ListTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, } from "../schemas.js";
4
+ import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadAttachmentRemoteSchema, DownloadJobArtifactsSchema, DownloadJobArtifactsRemoteSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, GetBranchSchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GetDeploymentSchema, GetDraftNoteSchema, GetEnvironmentSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetTimelineEventsSchema, GetUsersSchema, GetUserSchema, WhoAmISchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListBranchesSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListPipelinesSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListReleasesSchema, ListTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, } from "../schemas.js";
5
5
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
6
6
  // Define all available tools
7
7
  export const allTools = [
@@ -439,6 +439,11 @@ export const allTools = [
439
439
  description: "List projects accessible by the current user",
440
440
  inputSchema: toJSONSchema(ListProjectsSchema),
441
441
  },
442
+ {
443
+ name: "update_project",
444
+ description: "Update project settings such as description, visibility, default branch, and feature access levels",
445
+ inputSchema: toJSONSchema(UpdateProjectSchema),
446
+ },
442
447
  {
443
448
  name: "list_project_members",
444
449
  description: "List members of a GitLab project",
@@ -589,6 +594,16 @@ export const allTools = [
589
594
  description: "Validate an existing .gitlab-ci.yml configuration for a project",
590
595
  inputSchema: toJSONSchema(ValidateProjectCiLintSchema),
591
596
  },
597
+ {
598
+ name: "list_ci_catalog_resources",
599
+ description: "List GitLab CI/CD Catalog resources/components visible to the user",
600
+ inputSchema: toJSONSchema(ListCiCatalogResourcesSchema),
601
+ },
602
+ {
603
+ name: "get_ci_catalog_resource",
604
+ description: "Get details for a GitLab CI/CD Catalog resource, including versions and components",
605
+ inputSchema: toJSONSchema(GetCiCatalogResourceSchema),
606
+ },
592
607
  {
593
608
  name: "create_pipeline",
594
609
  description: "Create a new pipeline for a branch or tag",
@@ -1097,6 +1112,8 @@ export const readOnlyTools = new Set([
1097
1112
  "get_pipeline_job_output",
1098
1113
  "validate_ci_lint",
1099
1114
  "validate_project_ci_lint",
1115
+ "list_ci_catalog_resources",
1116
+ "get_ci_catalog_resource",
1100
1117
  "list_job_artifacts",
1101
1118
  "download_job_artifacts",
1102
1119
  "get_job_artifact_file",
@@ -1355,6 +1372,7 @@ export const TOOLSET_DEFINITIONS = [
1355
1372
  tools: new Set([
1356
1373
  "get_project",
1357
1374
  "list_projects",
1375
+ "update_project",
1358
1376
  "list_project_members",
1359
1377
  "list_namespaces",
1360
1378
  "get_namespace",
@@ -1378,7 +1396,12 @@ export const TOOLSET_DEFINITIONS = [
1378
1396
  {
1379
1397
  id: "ci",
1380
1398
  isDefault: true,
1381
- tools: new Set(["validate_ci_lint", "validate_project_ci_lint"]),
1399
+ tools: new Set([
1400
+ "validate_ci_lint",
1401
+ "validate_project_ci_lint",
1402
+ "list_ci_catalog_resources",
1403
+ "get_ci_catalog_resource",
1404
+ ]),
1382
1405
  },
1383
1406
  {
1384
1407
  id: "groups",