@zereight/mcp-gitlab 2.1.24 → 2.1.26
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.md +5 -0
- package/build/config.js +1 -0
- package/build/index.js +644 -324
- package/build/oauth.js +65 -3
- package/build/schemas.js +475 -197
- package/build/test/dynamic-api-url-test.js +3 -3
- package/build/test/oauth-tests.js +39 -0
- package/build/test/remote-auth-simple-test.js +13 -2
- package/build/test/schema-tests.js +51 -0
- package/build/test/streamable-http-concurrent-session.test.js +92 -0
- package/build/test/streamable-http-unauthenticated-discovery.test.js +113 -0
- package/build/test/test-ci-catalog.js +177 -0
- package/build/test/test-create-repository.js +120 -0
- package/build/test/test-list-issues.js +15 -3
- package/build/test/test-toolset-filtering.js +6 -5
- package/build/test/test-update-project.js +112 -0
- package/build/test/utils/forwarded-public-base-url.test.js +38 -0
- package/build/tools/registry.js +26 -3
- package/build/utils/forwarded-public-base-url.js +62 -0
- package/build/utils/schema.js +15 -1
- package/package.json +4 -2
|
@@ -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
|
|
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:
|
|
23
|
+
projects: 10,
|
|
24
24
|
labels: 5,
|
|
25
|
-
ci:
|
|
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
|
|
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
|
+
});
|
package/build/tools/registry.js
CHANGED
|
@@ -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 = [
|
|
@@ -426,7 +426,7 @@ export const allTools = [
|
|
|
426
426
|
},
|
|
427
427
|
{
|
|
428
428
|
name: "verify_namespace",
|
|
429
|
-
description: "Verify if a namespace path exists",
|
|
429
|
+
description: "Verify if a namespace path exists. Use parent_id to scope the check to a specific parent namespace — required for nested namespaces where the same path may exist under different parents.",
|
|
430
430
|
inputSchema: toJSONSchema(VerifyNamespaceSchema),
|
|
431
431
|
},
|
|
432
432
|
{
|
|
@@ -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([
|
|
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",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function getLastHeaderValue(value) {
|
|
2
|
+
const raw = Array.isArray(value) ? value[value.length - 1] : value;
|
|
3
|
+
if (!raw)
|
|
4
|
+
return undefined;
|
|
5
|
+
const parts = raw.split(",").map(part => part.trim()).filter(Boolean);
|
|
6
|
+
return parts.length > 0 ? parts[parts.length - 1] : undefined;
|
|
7
|
+
}
|
|
8
|
+
function unquoteHeaderValue(value) {
|
|
9
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
10
|
+
return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function trimTrailingSlashes(value) {
|
|
15
|
+
let end = value.length;
|
|
16
|
+
while (end > 0 && value[end - 1] === "/")
|
|
17
|
+
end--;
|
|
18
|
+
return value.slice(0, end);
|
|
19
|
+
}
|
|
20
|
+
export function getForwardedPublicBaseUrl(req, trustProxy) {
|
|
21
|
+
if (!trustProxy)
|
|
22
|
+
return undefined;
|
|
23
|
+
const forwarded = getLastHeaderValue(req.headers.forwarded);
|
|
24
|
+
const forwardedValues = {};
|
|
25
|
+
if (forwarded) {
|
|
26
|
+
for (const part of forwarded.split(";")) {
|
|
27
|
+
const separator = part.indexOf("=");
|
|
28
|
+
if (separator <= 0)
|
|
29
|
+
continue;
|
|
30
|
+
const key = part.slice(0, separator).trim().toLowerCase();
|
|
31
|
+
const value = unquoteHeaderValue(part.slice(separator + 1).trim());
|
|
32
|
+
if (key && value)
|
|
33
|
+
forwardedValues[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const proto = (forwardedValues.proto || getLastHeaderValue(req.headers["x-forwarded-proto"]))?.toLowerCase();
|
|
37
|
+
const host = forwardedValues.host || getLastHeaderValue(req.headers["x-forwarded-host"]);
|
|
38
|
+
if (!proto || !host || !/^https?$/.test(proto))
|
|
39
|
+
return undefined;
|
|
40
|
+
if (/[\s/\\]/.test(host))
|
|
41
|
+
return undefined;
|
|
42
|
+
const prefix = getLastHeaderValue(req.headers["x-forwarded-prefix"]);
|
|
43
|
+
const safePrefix = prefix &&
|
|
44
|
+
prefix.startsWith("/") &&
|
|
45
|
+
!prefix.startsWith("//") &&
|
|
46
|
+
!prefix.includes("://") &&
|
|
47
|
+
!/[\s\\]/.test(prefix)
|
|
48
|
+
? trimTrailingSlashes(prefix)
|
|
49
|
+
: undefined;
|
|
50
|
+
try {
|
|
51
|
+
const baseUrl = new URL(`${proto}://${host}`);
|
|
52
|
+
if (baseUrl.username || baseUrl.password || baseUrl.pathname !== "/" || baseUrl.search || baseUrl.hash) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (safePrefix)
|
|
56
|
+
baseUrl.pathname = safePrefix;
|
|
57
|
+
return baseUrl.toString().replace(/\/$/, "");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/build/utils/schema.js
CHANGED
|
@@ -86,5 +86,19 @@ export const toJSONSchema = (schema) => {
|
|
|
86
86
|
}
|
|
87
87
|
return obj;
|
|
88
88
|
}
|
|
89
|
-
|
|
89
|
+
const fixedSchema = fixNullableOptional(jsonSchema, true);
|
|
90
|
+
if (!fixedSchema.properties && Array.isArray(fixedSchema.anyOf)) {
|
|
91
|
+
const variants = fixedSchema.anyOf.filter((item) => item?.type === "object" && item.properties);
|
|
92
|
+
if (variants.length === fixedSchema.anyOf.length) {
|
|
93
|
+
fixedSchema.type = "object";
|
|
94
|
+
fixedSchema.properties = variants.reduce((properties, item) => {
|
|
95
|
+
Object.entries(item.properties).forEach(([key, value]) => {
|
|
96
|
+
if (!properties[key])
|
|
97
|
+
properties[key] = value;
|
|
98
|
+
});
|
|
99
|
+
return properties;
|
|
100
|
+
}, {});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return fixedSchema;
|
|
90
104
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.26",
|
|
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": [
|
|
@@ -51,16 +51,18 @@
|
|
|
51
51
|
"changelog": "auto-changelog -p",
|
|
52
52
|
"test": "npm run test:all",
|
|
53
53
|
"test:all": "npm run build && npm run test:mock && npm run test:live",
|
|
54
|
-
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/test-oauth-proxy-rate-limit.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-issues.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-remote-downloads.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-protected-branches.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/proxy-client-ip.test.ts && node --import tsx/esm --test test/utils/graphql-query.test.ts && node --import tsx/esm --test test/utils/wiki-title.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts && node --import tsx/esm --test test/test-ci-variables.ts && node --import tsx/esm --test test/test-dependency-proxy.ts",
|
|
54
|
+
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/test-oauth-proxy-rate-limit.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && node --import tsx/esm --test test/streamable-http-concurrent-session.test.ts && node --import tsx/esm --test test/streamable-http-unauthenticated-discovery.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-issues.ts && node --import tsx/esm --test test/test-create-repository.ts && node --import tsx/esm --test test/test-update-project.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-remote-downloads.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-protected-branches.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-ci-catalog.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/proxy-client-ip.test.ts && node --import tsx/esm --test test/utils/forwarded-public-base-url.test.ts && node --import tsx/esm --test test/utils/graphql-query.test.ts && node --import tsx/esm --test test/utils/wiki-title.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts && node --import tsx/esm --test test/test-ci-variables.ts && node --import tsx/esm --test test/test-dependency-proxy.ts",
|
|
55
55
|
"test:stateless": "npm run build && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
56
56
|
"test:mcp-oauth": "npm run build && node --import tsx/esm --test test/mcp-oauth-tests.ts",
|
|
57
57
|
"test:live": "node test/validate-api.js",
|
|
58
|
+
"test:consumer-smoke": "bash scripts/consumer-install-smoke.sh",
|
|
58
59
|
"test:remote-auth": "npm run build && node --import tsx/esm --test test/remote-auth-simple-test.ts",
|
|
59
60
|
"test:schema": "tsx test/schema-tests.ts && tsx test/test-json-schema.ts",
|
|
60
61
|
"test:oauth": "tsx test/oauth-tests.ts",
|
|
61
62
|
"test:list-merge-requests": "npm run build && tsx test/test-list-merge-requests.ts",
|
|
62
63
|
"test:approvals": "npm run build && tsx test/test-merge-request-approvals.ts",
|
|
63
64
|
"lint": "eslint . --ext .ts",
|
|
65
|
+
"check:runtime-deps": "node scripts/check-runtime-deps.mjs",
|
|
64
66
|
"lint:fix": "eslint . --ext .ts --fix",
|
|
65
67
|
"release": "bash scripts/release.sh",
|
|
66
68
|
"release:mcp-registry": "bash scripts/publish_mcp_registry.sh",
|