@zereight/mcp-gitlab 1.0.50 → 1.0.51
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 +8 -2
- package/build/index.js +43 -48
- package/build/schemas.js +154 -248
- package/build/scripts/generate-tools-readme.js +12 -12
- package/build/tests/integration.test.js +151 -0
- package/build/tests/unit.test.js +122 -0
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -26,7 +26,8 @@ When using with the Claude App, you need to set up your API key and URLs directl
|
|
|
26
26
|
"GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
|
|
27
27
|
"GITLAB_API_URL": "your_gitlab_api_url",
|
|
28
28
|
"GITLAB_READ_ONLY_MODE": "false",
|
|
29
|
-
"USE_GITLAB_WIKI": "
|
|
29
|
+
"USE_GITLAB_WIKI": "false",
|
|
30
|
+
"USE_MILESTONE": "false"
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -52,13 +53,16 @@ When using with the Claude App, you need to set up your API key and URLs directl
|
|
|
52
53
|
"GITLAB_READ_ONLY_MODE",
|
|
53
54
|
"-e",
|
|
54
55
|
"USE_GITLAB_WIKI",
|
|
56
|
+
"-e",
|
|
57
|
+
"USE_MILESTONE",
|
|
55
58
|
"iwakitakuma/gitlab-mcp"
|
|
56
59
|
],
|
|
57
60
|
"env": {
|
|
58
61
|
"GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
|
|
59
62
|
"GITLAB_API_URL": "https://gitlab.com/api/v4", // Optional, for self-hosted GitLab
|
|
60
63
|
"GITLAB_READ_ONLY_MODE": "false",
|
|
61
|
-
"USE_GITLAB_WIKI": "true"
|
|
64
|
+
"USE_GITLAB_WIKI": "true",
|
|
65
|
+
"USE_MILESTONE": "true"
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
}
|
|
@@ -77,10 +81,12 @@ $ sh scripts/image_push.sh docker_user_name
|
|
|
77
81
|
- `GITLAB_API_URL`: Your GitLab API URL. (Default: `https://gitlab.com/api/v4`)
|
|
78
82
|
- `GITLAB_READ_ONLY_MODE`: When set to 'true', restricts the server to only expose read-only operations. Useful for enhanced security or when write access is not needed. Also useful for using with Cursor and it's 40 tool limit.
|
|
79
83
|
- `USE_GITLAB_WIKI`: When set to 'true', enables the wiki-related tools (list_wiki_pages, get_wiki_page, create_wiki_page, update_wiki_page, delete_wiki_page). By default, wiki features are disabled.
|
|
84
|
+
- `USE_MILESTONE`: When set to 'true', enables the milestone-related tools (list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events). By default, milestone features are disabled.
|
|
80
85
|
|
|
81
86
|
## Tools 🛠️
|
|
82
87
|
|
|
83
88
|
+<!-- TOOLS-START -->
|
|
89
|
+
|
|
84
90
|
1. `create_or_update_file` - Create or update a single file in a GitLab project
|
|
85
91
|
2. `search_repositories` - Search for GitLab projects
|
|
86
92
|
3. `create_repository` - Create a new GitLab project
|
package/build/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import fetch from "node-fetch";
|
|
6
6
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
7
7
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
@@ -48,6 +48,7 @@ const server = new Server({
|
|
|
48
48
|
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
|
|
49
49
|
const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
|
|
50
50
|
const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true";
|
|
51
|
+
const USE_MILESTONE = process.env.USE_MILESTONE === "true";
|
|
51
52
|
// Add proxy configuration
|
|
52
53
|
const HTTP_PROXY = process.env.HTTP_PROXY;
|
|
53
54
|
const HTTPS_PROXY = process.env.HTTPS_PROXY;
|
|
@@ -421,6 +422,8 @@ const readOnlyTools = [
|
|
|
421
422
|
"get_milestone_issue",
|
|
422
423
|
"get_milestone_merge_requests",
|
|
423
424
|
"get_milestone_burndown_events",
|
|
425
|
+
"list_wiki_pages",
|
|
426
|
+
"get_wiki_page",
|
|
424
427
|
];
|
|
425
428
|
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
|
426
429
|
const wikiToolNames = [
|
|
@@ -431,6 +434,18 @@ const wikiToolNames = [
|
|
|
431
434
|
"delete_wiki_page",
|
|
432
435
|
"upload_wiki_attachment",
|
|
433
436
|
];
|
|
437
|
+
// Define which tools are related to milestones and can be toggled by USE_MILESTONE
|
|
438
|
+
const milestoneToolNames = [
|
|
439
|
+
"list_milestones",
|
|
440
|
+
"get_milestone",
|
|
441
|
+
"create_milestone",
|
|
442
|
+
"edit_milestone",
|
|
443
|
+
"delete_milestone",
|
|
444
|
+
"get_milestone_issue",
|
|
445
|
+
"get_milestone_merge_requests",
|
|
446
|
+
"promote_milestone",
|
|
447
|
+
"get_milestone_burndown_events",
|
|
448
|
+
];
|
|
434
449
|
/**
|
|
435
450
|
* Smart URL handling for GitLab API
|
|
436
451
|
*
|
|
@@ -444,8 +459,7 @@ function normalizeGitLabApiUrl(url) {
|
|
|
444
459
|
// Remove trailing slash if present
|
|
445
460
|
let normalizedUrl = url.endsWith("/") ? url.slice(0, -1) : url;
|
|
446
461
|
// Check if URL already has /api/v4
|
|
447
|
-
if (!normalizedUrl.endsWith("/api/v4") &&
|
|
448
|
-
!normalizedUrl.endsWith("/api/v4/")) {
|
|
462
|
+
if (!normalizedUrl.endsWith("/api/v4") && !normalizedUrl.endsWith("/api/v4/")) {
|
|
449
463
|
// Append /api/v4 if not already present
|
|
450
464
|
normalizedUrl = `${normalizedUrl}/api/v4`;
|
|
451
465
|
}
|
|
@@ -468,8 +482,7 @@ async function handleGitLabError(response) {
|
|
|
468
482
|
if (!response.ok) {
|
|
469
483
|
const errorBody = await response.text();
|
|
470
484
|
// Check specifically for Rate Limit error
|
|
471
|
-
if (response.status === 403 &&
|
|
472
|
-
errorBody.includes("User API Key Rate limit exceeded")) {
|
|
485
|
+
if (response.status === 403 && errorBody.includes("User API Key Rate limit exceeded")) {
|
|
473
486
|
console.error("GitLab API Rate Limit Exceeded:", errorBody);
|
|
474
487
|
console.log("User API Key Rate limit exceeded. Please try again later.");
|
|
475
488
|
throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
|
|
@@ -1095,7 +1108,7 @@ async function createTree(projectId, files, ref) {
|
|
|
1095
1108
|
...DEFAULT_FETCH_CONFIG,
|
|
1096
1109
|
method: "POST",
|
|
1097
1110
|
body: JSON.stringify({
|
|
1098
|
-
files: files.map(
|
|
1111
|
+
files: files.map(file => ({
|
|
1099
1112
|
file_path: file.path,
|
|
1100
1113
|
content: file.content,
|
|
1101
1114
|
encoding: "text",
|
|
@@ -1132,7 +1145,7 @@ async function createCommit(projectId, message, branch, actions) {
|
|
|
1132
1145
|
body: JSON.stringify({
|
|
1133
1146
|
branch,
|
|
1134
1147
|
commit_message: message,
|
|
1135
|
-
actions: actions.map(
|
|
1148
|
+
actions: actions.map(action => ({
|
|
1136
1149
|
action: "create",
|
|
1137
1150
|
file_path: action.path,
|
|
1138
1151
|
content: action.content,
|
|
@@ -1892,7 +1905,7 @@ async function listProjectMilestones(projectId, options) {
|
|
|
1892
1905
|
Object.entries(options).forEach(([key, value]) => {
|
|
1893
1906
|
if (value !== undefined) {
|
|
1894
1907
|
if (key === "iids" && Array.isArray(value) && value.length > 0) {
|
|
1895
|
-
value.forEach(
|
|
1908
|
+
value.forEach(iid => {
|
|
1896
1909
|
url.searchParams.append("iids[]", iid.toString());
|
|
1897
1910
|
});
|
|
1898
1911
|
}
|
|
@@ -2044,18 +2057,20 @@ async function getMilestoneBurndownEvents(projectId, milestoneId) {
|
|
|
2044
2057
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2045
2058
|
// Apply read-only filter first
|
|
2046
2059
|
const tools0 = GITLAB_READ_ONLY_MODE
|
|
2047
|
-
? allTools.filter(
|
|
2060
|
+
? allTools.filter(tool => readOnlyTools.includes(tool.name))
|
|
2048
2061
|
: allTools;
|
|
2049
2062
|
// Toggle wiki tools by USE_GITLAB_WIKI flag
|
|
2050
|
-
|
|
2063
|
+
const tools1 = USE_GITLAB_WIKI
|
|
2051
2064
|
? tools0
|
|
2052
|
-
: tools0.filter(
|
|
2065
|
+
: tools0.filter(tool => !wikiToolNames.includes(tool.name));
|
|
2066
|
+
// Toggle milestone tools by USE_MILESTONE flag
|
|
2067
|
+
let tools = USE_MILESTONE
|
|
2068
|
+
? tools1
|
|
2069
|
+
: tools1.filter(tool => !milestoneToolNames.includes(tool.name));
|
|
2053
2070
|
// <<< START: Gemini 호환성을 위해 $schema 제거 >>>
|
|
2054
|
-
tools = tools.map(
|
|
2071
|
+
tools = tools.map(tool => {
|
|
2055
2072
|
// inputSchema가 존재하고 객체인지 확인
|
|
2056
|
-
if (tool.inputSchema &&
|
|
2057
|
-
typeof tool.inputSchema === "object" &&
|
|
2058
|
-
tool.inputSchema !== null) {
|
|
2073
|
+
if (tool.inputSchema && typeof tool.inputSchema === "object" && tool.inputSchema !== null) {
|
|
2059
2074
|
// $schema 키가 존재하면 삭제
|
|
2060
2075
|
if ("$schema" in tool.inputSchema) {
|
|
2061
2076
|
// 불변성을 위해 새로운 객체 생성 (선택적이지만 권장)
|
|
@@ -2083,9 +2098,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2083
2098
|
try {
|
|
2084
2099
|
const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace);
|
|
2085
2100
|
return {
|
|
2086
|
-
content: [
|
|
2087
|
-
{ type: "text", text: JSON.stringify(forkedProject, null, 2) },
|
|
2088
|
-
],
|
|
2101
|
+
content: [{ type: "text", text: JSON.stringify(forkedProject, null, 2) }],
|
|
2089
2102
|
};
|
|
2090
2103
|
}
|
|
2091
2104
|
catch (forkError) {
|
|
@@ -2129,9 +2142,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2129
2142
|
const args = CreateRepositorySchema.parse(request.params.arguments);
|
|
2130
2143
|
const repository = await createRepository(args);
|
|
2131
2144
|
return {
|
|
2132
|
-
content: [
|
|
2133
|
-
{ type: "text", text: JSON.stringify(repository, null, 2) },
|
|
2134
|
-
],
|
|
2145
|
+
content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
|
|
2135
2146
|
};
|
|
2136
2147
|
}
|
|
2137
2148
|
case "get_file_contents": {
|
|
@@ -2150,7 +2161,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2150
2161
|
}
|
|
2151
2162
|
case "push_files": {
|
|
2152
2163
|
const args = PushFilesSchema.parse(request.params.arguments);
|
|
2153
|
-
const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(
|
|
2164
|
+
const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(f => ({ path: f.file_path, content: f.content })));
|
|
2154
2165
|
return {
|
|
2155
2166
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2156
2167
|
};
|
|
@@ -2168,9 +2179,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2168
2179
|
const { project_id, ...options } = args;
|
|
2169
2180
|
const mergeRequest = await createMergeRequest(project_id, options);
|
|
2170
2181
|
return {
|
|
2171
|
-
content: [
|
|
2172
|
-
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
|
|
2173
|
-
],
|
|
2182
|
+
content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
|
|
2174
2183
|
};
|
|
2175
2184
|
}
|
|
2176
2185
|
case "update_merge_request_note": {
|
|
@@ -2207,9 +2216,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2207
2216
|
const args = GetMergeRequestSchema.parse(request.params.arguments);
|
|
2208
2217
|
const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid, args.source_branch);
|
|
2209
2218
|
return {
|
|
2210
|
-
content: [
|
|
2211
|
-
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
|
|
2212
|
-
],
|
|
2219
|
+
content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
|
|
2213
2220
|
};
|
|
2214
2221
|
}
|
|
2215
2222
|
case "get_merge_request_diffs": {
|
|
@@ -2224,18 +2231,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2224
2231
|
const { project_id, merge_request_iid, source_branch, ...options } = args;
|
|
2225
2232
|
const mergeRequest = await updateMergeRequest(project_id, options, merge_request_iid, source_branch);
|
|
2226
2233
|
return {
|
|
2227
|
-
content: [
|
|
2228
|
-
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
|
|
2229
|
-
],
|
|
2234
|
+
content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
|
|
2230
2235
|
};
|
|
2231
2236
|
}
|
|
2232
2237
|
case "mr_discussions": {
|
|
2233
2238
|
const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments);
|
|
2234
2239
|
const discussions = await listMergeRequestDiscussions(args.project_id, args.merge_request_iid);
|
|
2235
2240
|
return {
|
|
2236
|
-
content: [
|
|
2237
|
-
{ type: "text", text: JSON.stringify(discussions, null, 2) },
|
|
2238
|
-
],
|
|
2241
|
+
content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
|
|
2239
2242
|
};
|
|
2240
2243
|
}
|
|
2241
2244
|
case "list_namespaces": {
|
|
@@ -2260,9 +2263,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2260
2263
|
const data = await response.json();
|
|
2261
2264
|
const namespaces = z.array(GitLabNamespaceSchema).parse(data);
|
|
2262
2265
|
return {
|
|
2263
|
-
content: [
|
|
2264
|
-
{ type: "text", text: JSON.stringify(namespaces, null, 2) },
|
|
2265
|
-
],
|
|
2266
|
+
content: [{ type: "text", text: JSON.stringify(namespaces, null, 2) }],
|
|
2266
2267
|
};
|
|
2267
2268
|
}
|
|
2268
2269
|
case "get_namespace": {
|
|
@@ -2288,9 +2289,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2288
2289
|
const data = await response.json();
|
|
2289
2290
|
const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data);
|
|
2290
2291
|
return {
|
|
2291
|
-
content: [
|
|
2292
|
-
{ type: "text", text: JSON.stringify(namespaceExists, null, 2) },
|
|
2293
|
-
],
|
|
2292
|
+
content: [{ type: "text", text: JSON.stringify(namespaceExists, null, 2) }],
|
|
2294
2293
|
};
|
|
2295
2294
|
}
|
|
2296
2295
|
case "get_project": {
|
|
@@ -2376,9 +2375,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2376
2375
|
const { project_id, issue_iid, ...options } = args;
|
|
2377
2376
|
const discussions = await listIssueDiscussions(project_id, issue_iid, options);
|
|
2378
2377
|
return {
|
|
2379
|
-
content: [
|
|
2380
|
-
{ type: "text", text: JSON.stringify(discussions, null, 2) },
|
|
2381
|
-
],
|
|
2378
|
+
content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
|
|
2382
2379
|
};
|
|
2383
2380
|
}
|
|
2384
2381
|
case "get_issue_link": {
|
|
@@ -2568,9 +2565,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2568
2565
|
const args = ListMergeRequestsSchema.parse(request.params.arguments);
|
|
2569
2566
|
const mergeRequests = await listMergeRequests(args.project_id, args);
|
|
2570
2567
|
return {
|
|
2571
|
-
content: [
|
|
2572
|
-
{ type: "text", text: JSON.stringify(mergeRequests, null, 2) },
|
|
2573
|
-
],
|
|
2568
|
+
content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }],
|
|
2574
2569
|
};
|
|
2575
2570
|
}
|
|
2576
2571
|
case "list_milestones": {
|
|
@@ -2691,7 +2686,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2691
2686
|
catch (error) {
|
|
2692
2687
|
if (error instanceof z.ZodError) {
|
|
2693
2688
|
throw new Error(`Invalid arguments: ${error.errors
|
|
2694
|
-
.map(
|
|
2689
|
+
.map(e => `${e.path.join(".")}: ${e.message}`)
|
|
2695
2690
|
.join(", ")}`);
|
|
2696
2691
|
}
|
|
2697
2692
|
throw error;
|
|
@@ -2716,7 +2711,7 @@ async function runServer() {
|
|
|
2716
2711
|
process.exit(1);
|
|
2717
2712
|
}
|
|
2718
2713
|
}
|
|
2719
|
-
runServer().catch(
|
|
2714
|
+
runServer().catch(error => {
|
|
2720
2715
|
console.error("Fatal error in main():", error);
|
|
2721
2716
|
process.exit(1);
|
|
2722
2717
|
});
|
package/build/schemas.js
CHANGED
|
@@ -20,13 +20,16 @@ export const GitLabPipelineSchema = z.object({
|
|
|
20
20
|
started_at: z.string().nullable().optional(),
|
|
21
21
|
finished_at: z.string().nullable().optional(),
|
|
22
22
|
coverage: z.number().nullable().optional(),
|
|
23
|
-
user: z
|
|
23
|
+
user: z
|
|
24
|
+
.object({
|
|
24
25
|
id: z.number(),
|
|
25
26
|
name: z.string(),
|
|
26
27
|
username: z.string(),
|
|
27
28
|
avatar_url: z.string().nullable().optional(),
|
|
28
|
-
})
|
|
29
|
-
|
|
29
|
+
})
|
|
30
|
+
.optional(),
|
|
31
|
+
detailed_status: z
|
|
32
|
+
.object({
|
|
30
33
|
icon: z.string().optional(),
|
|
31
34
|
text: z.string().optional(),
|
|
32
35
|
label: z.string().optional(),
|
|
@@ -34,13 +37,17 @@ export const GitLabPipelineSchema = z.object({
|
|
|
34
37
|
tooltip: z.string().optional(),
|
|
35
38
|
has_details: z.boolean().optional(),
|
|
36
39
|
details_path: z.string().optional(),
|
|
37
|
-
illustration: z
|
|
40
|
+
illustration: z
|
|
41
|
+
.object({
|
|
38
42
|
image: z.string().optional(),
|
|
39
43
|
size: z.string().optional(),
|
|
40
44
|
title: z.string().optional(),
|
|
41
|
-
})
|
|
45
|
+
})
|
|
46
|
+
.nullable()
|
|
47
|
+
.optional(),
|
|
42
48
|
favicon: z.string().optional(),
|
|
43
|
-
})
|
|
49
|
+
})
|
|
50
|
+
.optional(),
|
|
44
51
|
});
|
|
45
52
|
// Pipeline job related schemas
|
|
46
53
|
export const GitLabPipelineJobSchema = z.object({
|
|
@@ -55,41 +62,74 @@ export const GitLabPipelineJobSchema = z.object({
|
|
|
55
62
|
started_at: z.string().nullable().optional(),
|
|
56
63
|
finished_at: z.string().nullable().optional(),
|
|
57
64
|
duration: z.number().nullable().optional(),
|
|
58
|
-
user: z
|
|
65
|
+
user: z
|
|
66
|
+
.object({
|
|
59
67
|
id: z.number(),
|
|
60
68
|
name: z.string(),
|
|
61
69
|
username: z.string(),
|
|
62
70
|
avatar_url: z.string().nullable().optional(),
|
|
63
|
-
})
|
|
64
|
-
|
|
71
|
+
})
|
|
72
|
+
.optional(),
|
|
73
|
+
commit: z
|
|
74
|
+
.object({
|
|
65
75
|
id: z.string(),
|
|
66
76
|
short_id: z.string(),
|
|
67
77
|
title: z.string(),
|
|
68
78
|
author_name: z.string(),
|
|
69
79
|
author_email: z.string(),
|
|
70
|
-
})
|
|
71
|
-
|
|
80
|
+
})
|
|
81
|
+
.optional(),
|
|
82
|
+
pipeline: z
|
|
83
|
+
.object({
|
|
72
84
|
id: z.number(),
|
|
73
85
|
project_id: z.number(),
|
|
74
86
|
status: z.string(),
|
|
75
87
|
ref: z.string(),
|
|
76
88
|
sha: z.string(),
|
|
77
|
-
})
|
|
89
|
+
})
|
|
90
|
+
.optional(),
|
|
78
91
|
web_url: z.string().optional(),
|
|
79
92
|
});
|
|
80
93
|
// Schema for listing pipelines
|
|
81
94
|
export const ListPipelinesSchema = z.object({
|
|
82
95
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
83
|
-
scope: z
|
|
84
|
-
|
|
96
|
+
scope: z
|
|
97
|
+
.enum(["running", "pending", "finished", "branches", "tags"])
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("The scope of pipelines"),
|
|
100
|
+
status: z
|
|
101
|
+
.enum([
|
|
102
|
+
"created",
|
|
103
|
+
"waiting_for_resource",
|
|
104
|
+
"preparing",
|
|
105
|
+
"pending",
|
|
106
|
+
"running",
|
|
107
|
+
"success",
|
|
108
|
+
"failed",
|
|
109
|
+
"canceled",
|
|
110
|
+
"skipped",
|
|
111
|
+
"manual",
|
|
112
|
+
"scheduled",
|
|
113
|
+
])
|
|
114
|
+
.optional()
|
|
115
|
+
.describe("The status of pipelines"),
|
|
85
116
|
ref: z.string().optional().describe("The ref of pipelines"),
|
|
86
117
|
sha: z.string().optional().describe("The SHA of pipelines"),
|
|
87
118
|
yaml_errors: z.boolean().optional().describe("Returns pipelines with invalid configurations"),
|
|
88
119
|
username: z.string().optional().describe("The username of the user who triggered pipelines"),
|
|
89
|
-
updated_after: z
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
updated_after: z
|
|
121
|
+
.string()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Return pipelines updated after the specified date"),
|
|
124
|
+
updated_before: z
|
|
125
|
+
.string()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe("Return pipelines updated before the specified date"),
|
|
128
|
+
order_by: z
|
|
129
|
+
.enum(["id", "status", "ref", "updated_at", "user_id"])
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Order pipelines by"),
|
|
132
|
+
sort: z.enum(["asc", "desc"]).optional().describe("Sort pipelines"),
|
|
93
133
|
page: z.number().optional().describe("Page number for pagination"),
|
|
94
134
|
per_page: z.number().optional().describe("Number of items per page (max 100)"),
|
|
95
135
|
});
|
|
@@ -102,7 +142,10 @@ export const GetPipelineSchema = z.object({
|
|
|
102
142
|
export const ListPipelineJobsSchema = z.object({
|
|
103
143
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
104
144
|
pipeline_id: z.number().describe("The ID of the pipeline"),
|
|
105
|
-
scope: z
|
|
145
|
+
scope: z
|
|
146
|
+
.enum(["created", "pending", "running", "failed", "success", "canceled", "skipped", "manual"])
|
|
147
|
+
.optional()
|
|
148
|
+
.describe("The scope of jobs to show"),
|
|
106
149
|
include_retried: z.boolean().optional().describe("Whether to include retried jobs"),
|
|
107
150
|
page: z.number().optional().describe("Page number for pagination"),
|
|
108
151
|
per_page: z.number().optional().describe("Number of items per page (max 100)"),
|
|
@@ -267,18 +310,9 @@ export const GetRepositoryTreeSchema = z.object({
|
|
|
267
310
|
.string()
|
|
268
311
|
.optional()
|
|
269
312
|
.describe("The name of a repository branch or tag. Defaults to the default branch."),
|
|
270
|
-
recursive: z
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
.describe("Boolean value to get a recursive tree"),
|
|
274
|
-
per_page: z
|
|
275
|
-
.number()
|
|
276
|
-
.optional()
|
|
277
|
-
.describe("Number of results to show per page"),
|
|
278
|
-
page_token: z
|
|
279
|
-
.string()
|
|
280
|
-
.optional()
|
|
281
|
-
.describe("The tree record ID for pagination"),
|
|
313
|
+
recursive: z.boolean().optional().describe("Boolean value to get a recursive tree"),
|
|
314
|
+
per_page: z.number().optional().describe("Number of results to show per page"),
|
|
315
|
+
page_token: z.string().optional().describe("The tree record ID for pagination"),
|
|
282
316
|
pagination: z.string().optional().describe("Pagination method (keyset)"),
|
|
283
317
|
});
|
|
284
318
|
export const GitLabTreeSchema = z.object({
|
|
@@ -319,7 +353,7 @@ export const GitLabMilestonesSchema = z.object({
|
|
|
319
353
|
updated_at: z.string(),
|
|
320
354
|
created_at: z.string(),
|
|
321
355
|
expired: z.boolean(),
|
|
322
|
-
web_url: z.string().optional()
|
|
356
|
+
web_url: z.string().optional(),
|
|
323
357
|
});
|
|
324
358
|
// Input schemas for operations
|
|
325
359
|
export const CreateRepositoryOptionsSchema = z.object({
|
|
@@ -558,10 +592,12 @@ export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({
|
|
|
558
592
|
note_id: z.number().describe("The ID of a thread note"),
|
|
559
593
|
body: z.string().optional().describe("The content of the note or reply"),
|
|
560
594
|
resolved: z.boolean().optional().describe("Resolve or unresolve the note"),
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
595
|
+
})
|
|
596
|
+
.refine(data => data.body !== undefined || data.resolved !== undefined, {
|
|
597
|
+
message: "At least one of 'body' or 'resolved' must be provided",
|
|
598
|
+
})
|
|
599
|
+
.refine(data => !(data.body !== undefined && data.resolved !== undefined), {
|
|
600
|
+
message: "Only one of 'body' or 'resolved' can be provided, not both",
|
|
565
601
|
});
|
|
566
602
|
// Input schema for adding a note to an existing merge request discussion
|
|
567
603
|
export const CreateMergeRequestNoteSchema = ProjectParamsSchema.extend({
|
|
@@ -590,26 +626,14 @@ export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
|
|
|
590
626
|
content: z.string().describe("Content of the file"),
|
|
591
627
|
commit_message: z.string().describe("Commit message"),
|
|
592
628
|
branch: z.string().describe("Branch to create/update the file in"),
|
|
593
|
-
previous_path: z
|
|
594
|
-
.string()
|
|
595
|
-
.optional()
|
|
596
|
-
.describe("Path of the file to move/rename"),
|
|
629
|
+
previous_path: z.string().optional().describe("Path of the file to move/rename"),
|
|
597
630
|
last_commit_id: z.string().optional().describe("Last known file commit ID"),
|
|
598
|
-
commit_id: z
|
|
599
|
-
.string()
|
|
600
|
-
.optional()
|
|
601
|
-
.describe("Current file commit ID (for update operations)"),
|
|
631
|
+
commit_id: z.string().optional().describe("Current file commit ID (for update operations)"),
|
|
602
632
|
});
|
|
603
633
|
export const SearchRepositoriesSchema = z.object({
|
|
604
634
|
search: z.string().describe("Search query"), // Changed from query to match GitLab API
|
|
605
|
-
page: z
|
|
606
|
-
|
|
607
|
-
.optional()
|
|
608
|
-
.describe("Page number for pagination (default: 1)"),
|
|
609
|
-
per_page: z
|
|
610
|
-
.number()
|
|
611
|
-
.optional()
|
|
612
|
-
.describe("Number of results per page (default: 20)"),
|
|
635
|
+
page: z.number().optional().describe("Page number for pagination (default: 1)"),
|
|
636
|
+
per_page: z.number().optional().describe("Number of results per page (default: 20)"),
|
|
613
637
|
});
|
|
614
638
|
export const CreateRepositorySchema = z.object({
|
|
615
639
|
name: z.string().describe("Repository name"),
|
|
@@ -618,10 +642,7 @@ export const CreateRepositorySchema = z.object({
|
|
|
618
642
|
.enum(["private", "internal", "public"])
|
|
619
643
|
.optional()
|
|
620
644
|
.describe("Repository visibility level"),
|
|
621
|
-
initialize_with_readme: z
|
|
622
|
-
.boolean()
|
|
623
|
-
.optional()
|
|
624
|
-
.describe("Initialize with README.md"),
|
|
645
|
+
initialize_with_readme: z.boolean().optional().describe("Initialize with README.md"),
|
|
625
646
|
});
|
|
626
647
|
export const GetFileContentsSchema = ProjectParamsSchema.extend({
|
|
627
648
|
file_path: z.string().describe("Path to the file or directory"),
|
|
@@ -640,10 +661,7 @@ export const PushFilesSchema = ProjectParamsSchema.extend({
|
|
|
640
661
|
export const CreateIssueSchema = ProjectParamsSchema.extend({
|
|
641
662
|
title: z.string().describe("Issue title"),
|
|
642
663
|
description: z.string().optional().describe("Issue description"),
|
|
643
|
-
assignee_ids: z
|
|
644
|
-
.array(z.number())
|
|
645
|
-
.optional()
|
|
646
|
-
.describe("Array of user IDs to assign"),
|
|
664
|
+
assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"),
|
|
647
665
|
labels: z.array(z.string()).optional().describe("Array of label names"),
|
|
648
666
|
milestone_id: z.number().optional().describe("Milestone ID to assign"),
|
|
649
667
|
});
|
|
@@ -653,10 +671,7 @@ export const CreateMergeRequestSchema = ProjectParamsSchema.extend({
|
|
|
653
671
|
source_branch: z.string().describe("Branch containing changes"),
|
|
654
672
|
target_branch: z.string().describe("Branch to merge into"),
|
|
655
673
|
draft: z.boolean().optional().describe("Create as draft merge request"),
|
|
656
|
-
allow_collaboration: z
|
|
657
|
-
.boolean()
|
|
658
|
-
.optional()
|
|
659
|
-
.describe("Allow commits from upstream members"),
|
|
674
|
+
allow_collaboration: z.boolean().optional().describe("Allow commits from upstream members"),
|
|
660
675
|
});
|
|
661
676
|
export const ForkRepositorySchema = ProjectParamsSchema.extend({
|
|
662
677
|
namespace: z.string().optional().describe("Namespace to fork to (full path)"),
|
|
@@ -676,23 +691,14 @@ export const GitLabMergeRequestDiffSchema = z.object({
|
|
|
676
691
|
deleted_file: z.boolean(),
|
|
677
692
|
});
|
|
678
693
|
export const GetMergeRequestSchema = ProjectParamsSchema.extend({
|
|
679
|
-
merge_request_iid: z
|
|
680
|
-
.number()
|
|
681
|
-
.optional()
|
|
682
|
-
.describe("The IID of a merge request"),
|
|
694
|
+
merge_request_iid: z.number().optional().describe("The IID of a merge request"),
|
|
683
695
|
source_branch: z.string().optional().describe("Source branch name"),
|
|
684
696
|
});
|
|
685
697
|
export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
|
|
686
698
|
title: z.string().optional().describe("The title of the merge request"),
|
|
687
|
-
description: z
|
|
688
|
-
.string()
|
|
689
|
-
.optional()
|
|
690
|
-
.describe("The description of the merge request"),
|
|
699
|
+
description: z.string().optional().describe("The description of the merge request"),
|
|
691
700
|
target_branch: z.string().optional().describe("The target branch"),
|
|
692
|
-
assignee_ids: z
|
|
693
|
-
.array(z.number())
|
|
694
|
-
.optional()
|
|
695
|
-
.describe("The ID of the users to assign the MR to"),
|
|
701
|
+
assignee_ids: z.array(z.number()).optional().describe("The ID of the users to assign the MR to"),
|
|
696
702
|
labels: z.array(z.string()).optional().describe("Labels for the MR"),
|
|
697
703
|
state_event: z
|
|
698
704
|
.enum(["close", "reopen"])
|
|
@@ -702,10 +708,7 @@ export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
|
|
|
702
708
|
.boolean()
|
|
703
709
|
.optional()
|
|
704
710
|
.describe("Flag indicating if the source branch should be removed"),
|
|
705
|
-
squash: z
|
|
706
|
-
.boolean()
|
|
707
|
-
.optional()
|
|
708
|
-
.describe("Squash commits into a single commit when merging"),
|
|
711
|
+
squash: z.boolean().optional().describe("Squash commits into a single commit when merging"),
|
|
709
712
|
draft: z.boolean().optional().describe("Work in progress merge request"),
|
|
710
713
|
});
|
|
711
714
|
export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
|
|
@@ -722,38 +725,14 @@ export const CreateNoteSchema = z.object({
|
|
|
722
725
|
// Issues API operation schemas
|
|
723
726
|
export const ListIssuesSchema = z.object({
|
|
724
727
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
725
|
-
assignee_id: z
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
author_id: z
|
|
734
|
-
.number()
|
|
735
|
-
.optional()
|
|
736
|
-
.describe("Return issues created by the given user ID"),
|
|
737
|
-
author_username: z
|
|
738
|
-
.string()
|
|
739
|
-
.optional()
|
|
740
|
-
.describe("Return issues created by the given username"),
|
|
741
|
-
confidential: z
|
|
742
|
-
.boolean()
|
|
743
|
-
.optional()
|
|
744
|
-
.describe("Filter confidential or public issues"),
|
|
745
|
-
created_after: z
|
|
746
|
-
.string()
|
|
747
|
-
.optional()
|
|
748
|
-
.describe("Return issues created after the given time"),
|
|
749
|
-
created_before: z
|
|
750
|
-
.string()
|
|
751
|
-
.optional()
|
|
752
|
-
.describe("Return issues created before the given time"),
|
|
753
|
-
due_date: z
|
|
754
|
-
.string()
|
|
755
|
-
.optional()
|
|
756
|
-
.describe("Return issues that have the due date"),
|
|
728
|
+
assignee_id: z.number().optional().describe("Return issues assigned to the given user ID"),
|
|
729
|
+
assignee_username: z.string().optional().describe("Return issues assigned to the given username"),
|
|
730
|
+
author_id: z.number().optional().describe("Return issues created by the given user ID"),
|
|
731
|
+
author_username: z.string().optional().describe("Return issues created by the given username"),
|
|
732
|
+
confidential: z.boolean().optional().describe("Filter confidential or public issues"),
|
|
733
|
+
created_after: z.string().optional().describe("Return issues created after the given time"),
|
|
734
|
+
created_before: z.string().optional().describe("Return issues created before the given time"),
|
|
735
|
+
due_date: z.string().optional().describe("Return issues that have the due date"),
|
|
757
736
|
label_name: z.array(z.string()).optional().describe("Array of label names"),
|
|
758
737
|
milestone: z.string().optional().describe("Milestone title"),
|
|
759
738
|
scope: z
|
|
@@ -765,18 +744,9 @@ export const ListIssuesSchema = z.object({
|
|
|
765
744
|
.enum(["opened", "closed", "all"])
|
|
766
745
|
.optional()
|
|
767
746
|
.describe("Return issues with a specific state"),
|
|
768
|
-
updated_after: z
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
.describe("Return issues updated after the given time"),
|
|
772
|
-
updated_before: z
|
|
773
|
-
.string()
|
|
774
|
-
.optional()
|
|
775
|
-
.describe("Return issues updated before the given time"),
|
|
776
|
-
with_labels_details: z
|
|
777
|
-
.boolean()
|
|
778
|
-
.optional()
|
|
779
|
-
.describe("Return more details for each label"),
|
|
747
|
+
updated_after: z.string().optional().describe("Return issues updated after the given time"),
|
|
748
|
+
updated_before: z.string().optional().describe("Return issues updated before the given time"),
|
|
749
|
+
with_labels_details: z.boolean().optional().describe("Return more details for each label"),
|
|
780
750
|
page: z.number().optional().describe("Page number for pagination"),
|
|
781
751
|
per_page: z.number().optional().describe("Number of items per page"),
|
|
782
752
|
});
|
|
@@ -791,10 +761,7 @@ export const ListMergeRequestsSchema = z.object({
|
|
|
791
761
|
.string()
|
|
792
762
|
.optional()
|
|
793
763
|
.describe("Returns merge requests assigned to the given username"),
|
|
794
|
-
author_id: z
|
|
795
|
-
.number()
|
|
796
|
-
.optional()
|
|
797
|
-
.describe("Returns merge requests created by the given user ID"),
|
|
764
|
+
author_id: z.number().optional().describe("Returns merge requests created by the given user ID"),
|
|
798
765
|
author_username: z
|
|
799
766
|
.string()
|
|
800
767
|
.optional()
|
|
@@ -850,14 +817,8 @@ export const ListMergeRequestsSchema = z.object({
|
|
|
850
817
|
.string()
|
|
851
818
|
.optional()
|
|
852
819
|
.describe("Return merge requests from a specific source branch"),
|
|
853
|
-
wip: z
|
|
854
|
-
|
|
855
|
-
.optional()
|
|
856
|
-
.describe("Filter merge requests against their wip status"),
|
|
857
|
-
with_labels_details: z
|
|
858
|
-
.boolean()
|
|
859
|
-
.optional()
|
|
860
|
-
.describe("Return more details for each label"),
|
|
820
|
+
wip: z.enum(["yes", "no"]).optional().describe("Filter merge requests against their wip status"),
|
|
821
|
+
with_labels_details: z.boolean().optional().describe("Return more details for each label"),
|
|
861
822
|
page: z.number().optional().describe("Page number for pagination"),
|
|
862
823
|
per_page: z.number().optional().describe("Number of items per page"),
|
|
863
824
|
});
|
|
@@ -870,28 +831,13 @@ export const UpdateIssueSchema = z.object({
|
|
|
870
831
|
issue_iid: z.number().describe("The internal ID of the project issue"),
|
|
871
832
|
title: z.string().optional().describe("The title of the issue"),
|
|
872
833
|
description: z.string().optional().describe("The description of the issue"),
|
|
873
|
-
assignee_ids: z
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
confidential: z
|
|
878
|
-
.boolean()
|
|
879
|
-
.optional()
|
|
880
|
-
.describe("Set the issue to be confidential"),
|
|
881
|
-
discussion_locked: z
|
|
882
|
-
.boolean()
|
|
883
|
-
.optional()
|
|
884
|
-
.describe("Flag to lock discussions"),
|
|
885
|
-
due_date: z
|
|
886
|
-
.string()
|
|
887
|
-
.optional()
|
|
888
|
-
.describe("Date the issue is due (YYYY-MM-DD)"),
|
|
834
|
+
assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign issue to"),
|
|
835
|
+
confidential: z.boolean().optional().describe("Set the issue to be confidential"),
|
|
836
|
+
discussion_locked: z.boolean().optional().describe("Flag to lock discussions"),
|
|
837
|
+
due_date: z.string().optional().describe("Date the issue is due (YYYY-MM-DD)"),
|
|
889
838
|
labels: z.array(z.string()).optional().describe("Array of label names"),
|
|
890
839
|
milestone_id: z.number().optional().describe("Milestone ID to assign"),
|
|
891
|
-
state_event: z
|
|
892
|
-
.enum(["close", "reopen"])
|
|
893
|
-
.optional()
|
|
894
|
-
.describe("Update issue state (close/reopen)"),
|
|
840
|
+
state_event: z.enum(["close", "reopen"]).optional().describe("Update issue state (close/reopen)"),
|
|
895
841
|
weight: z.number().optional().describe("Weight of the issue (0-9)"),
|
|
896
842
|
});
|
|
897
843
|
export const DeleteIssueSchema = z.object({
|
|
@@ -913,8 +859,14 @@ export const ListIssueDiscussionsSchema = z.object({
|
|
|
913
859
|
issue_iid: z.number().describe("The internal ID of the project issue"),
|
|
914
860
|
page: z.number().optional().describe("Page number for pagination"),
|
|
915
861
|
per_page: z.number().optional().describe("Number of items per page"),
|
|
916
|
-
sort: z
|
|
917
|
-
|
|
862
|
+
sort: z
|
|
863
|
+
.enum(["asc", "desc"])
|
|
864
|
+
.optional()
|
|
865
|
+
.describe("Return issue discussions sorted in ascending or descending order"),
|
|
866
|
+
order_by: z
|
|
867
|
+
.enum(["created_at", "updated_at"])
|
|
868
|
+
.optional()
|
|
869
|
+
.describe("Return issue discussions ordered by created_at or updated_at fields"),
|
|
918
870
|
});
|
|
919
871
|
export const GetIssueLinkSchema = z.object({
|
|
920
872
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
@@ -924,12 +876,8 @@ export const GetIssueLinkSchema = z.object({
|
|
|
924
876
|
export const CreateIssueLinkSchema = z.object({
|
|
925
877
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
926
878
|
issue_iid: z.number().describe("The internal ID of a project's issue"),
|
|
927
|
-
target_project_id: z
|
|
928
|
-
|
|
929
|
-
.describe("The ID or URL-encoded path of a target project"),
|
|
930
|
-
target_issue_iid: z
|
|
931
|
-
.number()
|
|
932
|
-
.describe("The internal ID of a target project's issue"),
|
|
879
|
+
target_project_id: z.string().describe("The ID or URL-encoded path of a target project"),
|
|
880
|
+
target_issue_iid: z.number().describe("The internal ID of a target project's issue"),
|
|
933
881
|
link_type: z
|
|
934
882
|
.enum(["relates_to", "blocks", "is_blocked_by"])
|
|
935
883
|
.optional()
|
|
@@ -945,10 +893,7 @@ export const ListNamespacesSchema = z.object({
|
|
|
945
893
|
search: z.string().optional().describe("Search term for namespaces"),
|
|
946
894
|
page: z.number().optional().describe("Page number for pagination"),
|
|
947
895
|
per_page: z.number().optional().describe("Number of items per page"),
|
|
948
|
-
owned: z
|
|
949
|
-
.boolean()
|
|
950
|
-
.optional()
|
|
951
|
-
.describe("Filter for namespaces owned by current user"),
|
|
896
|
+
owned: z.boolean().optional().describe("Filter for namespaces owned by current user"),
|
|
952
897
|
});
|
|
953
898
|
export const GetNamespaceSchema = z.object({
|
|
954
899
|
namespace_id: z.string().describe("Namespace ID or full path"),
|
|
@@ -964,18 +909,9 @@ export const ListProjectsSchema = z.object({
|
|
|
964
909
|
search: z.string().optional().describe("Search term for projects"),
|
|
965
910
|
page: z.number().optional().describe("Page number for pagination"),
|
|
966
911
|
per_page: z.number().optional().describe("Number of items per page"),
|
|
967
|
-
search_namespaces: z
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
.describe("Needs to be true if search is full path"),
|
|
971
|
-
owned: z
|
|
972
|
-
.boolean()
|
|
973
|
-
.optional()
|
|
974
|
-
.describe("Filter for projects owned by current user"),
|
|
975
|
-
membership: z
|
|
976
|
-
.boolean()
|
|
977
|
-
.optional()
|
|
978
|
-
.describe("Filter for projects where current user is a member"),
|
|
912
|
+
search_namespaces: z.boolean().optional().describe("Needs to be true if search is full path"),
|
|
913
|
+
owned: z.boolean().optional().describe("Filter for projects owned by current user"),
|
|
914
|
+
membership: z.boolean().optional().describe("Filter for projects where current user is a member"),
|
|
979
915
|
simple: z.boolean().optional().describe("Return only limited fields"),
|
|
980
916
|
archived: z.boolean().optional().describe("Filter for archived projects"),
|
|
981
917
|
visibility: z
|
|
@@ -983,14 +919,7 @@ export const ListProjectsSchema = z.object({
|
|
|
983
919
|
.optional()
|
|
984
920
|
.describe("Filter by project visibility"),
|
|
985
921
|
order_by: z
|
|
986
|
-
.enum([
|
|
987
|
-
"id",
|
|
988
|
-
"name",
|
|
989
|
-
"path",
|
|
990
|
-
"created_at",
|
|
991
|
-
"updated_at",
|
|
992
|
-
"last_activity_at",
|
|
993
|
-
])
|
|
922
|
+
.enum(["id", "name", "path", "created_at", "updated_at", "last_activity_at"])
|
|
994
923
|
.optional()
|
|
995
924
|
.describe("Return projects ordered by field"),
|
|
996
925
|
sort: z
|
|
@@ -1005,10 +934,7 @@ export const ListProjectsSchema = z.object({
|
|
|
1005
934
|
.boolean()
|
|
1006
935
|
.optional()
|
|
1007
936
|
.describe("Filter projects with merge requests feature enabled"),
|
|
1008
|
-
min_access_level: z
|
|
1009
|
-
.number()
|
|
1010
|
-
.optional()
|
|
1011
|
-
.describe("Filter by minimum access level"),
|
|
937
|
+
min_access_level: z.number().optional().describe("Filter by minimum access level"),
|
|
1012
938
|
});
|
|
1013
939
|
// Label operation schemas
|
|
1014
940
|
export const ListLabelsSchema = z.object({
|
|
@@ -1017,19 +943,13 @@ export const ListLabelsSchema = z.object({
|
|
|
1017
943
|
.boolean()
|
|
1018
944
|
.optional()
|
|
1019
945
|
.describe("Whether or not to include issue and merge request counts"),
|
|
1020
|
-
include_ancestor_groups: z
|
|
1021
|
-
.boolean()
|
|
1022
|
-
.optional()
|
|
1023
|
-
.describe("Include ancestor groups"),
|
|
946
|
+
include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"),
|
|
1024
947
|
search: z.string().optional().describe("Keyword to filter labels by"),
|
|
1025
948
|
});
|
|
1026
949
|
export const GetLabelSchema = z.object({
|
|
1027
950
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
1028
951
|
label_id: z.string().describe("The ID or title of a project's label"),
|
|
1029
|
-
include_ancestor_groups: z
|
|
1030
|
-
.boolean()
|
|
1031
|
-
.optional()
|
|
1032
|
-
.describe("Include ancestor groups"),
|
|
952
|
+
include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"),
|
|
1033
953
|
});
|
|
1034
954
|
export const CreateLabelSchema = z.object({
|
|
1035
955
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
@@ -1038,11 +958,7 @@ export const CreateLabelSchema = z.object({
|
|
|
1038
958
|
.string()
|
|
1039
959
|
.describe("The color of the label given in 6-digit hex notation with leading '#' sign"),
|
|
1040
960
|
description: z.string().optional().describe("The description of the label"),
|
|
1041
|
-
priority: z
|
|
1042
|
-
.number()
|
|
1043
|
-
.nullable()
|
|
1044
|
-
.optional()
|
|
1045
|
-
.describe("The priority of the label"),
|
|
961
|
+
priority: z.number().nullable().optional().describe("The priority of the label"),
|
|
1046
962
|
});
|
|
1047
963
|
export const UpdateLabelSchema = z.object({
|
|
1048
964
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
@@ -1052,15 +968,8 @@ export const UpdateLabelSchema = z.object({
|
|
|
1052
968
|
.string()
|
|
1053
969
|
.optional()
|
|
1054
970
|
.describe("The color of the label given in 6-digit hex notation with leading '#' sign"),
|
|
1055
|
-
description: z
|
|
1056
|
-
|
|
1057
|
-
.optional()
|
|
1058
|
-
.describe("The new description of the label"),
|
|
1059
|
-
priority: z
|
|
1060
|
-
.number()
|
|
1061
|
-
.nullable()
|
|
1062
|
-
.optional()
|
|
1063
|
-
.describe("The new priority of the label"),
|
|
971
|
+
description: z.string().optional().describe("The new description of the label"),
|
|
972
|
+
priority: z.number().nullable().optional().describe("The new priority of the label"),
|
|
1064
973
|
});
|
|
1065
974
|
export const DeleteLabelSchema = z.object({
|
|
1066
975
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
@@ -1069,10 +978,7 @@ export const DeleteLabelSchema = z.object({
|
|
|
1069
978
|
// Group projects schema
|
|
1070
979
|
export const ListGroupProjectsSchema = z.object({
|
|
1071
980
|
group_id: z.string().describe("Group ID or path"),
|
|
1072
|
-
include_subgroups: z
|
|
1073
|
-
.boolean()
|
|
1074
|
-
.optional()
|
|
1075
|
-
.describe("Include projects from subgroups"),
|
|
981
|
+
include_subgroups: z.boolean().optional().describe("Include projects from subgroups"),
|
|
1076
982
|
search: z.string().optional().describe("Search term to filter projects"),
|
|
1077
983
|
order_by: z
|
|
1078
984
|
.enum(["name", "path", "created_at", "updated_at", "last_activity_at"])
|
|
@@ -1094,24 +1000,12 @@ export const ListGroupProjectsSchema = z.object({
|
|
|
1094
1000
|
.boolean()
|
|
1095
1001
|
.optional()
|
|
1096
1002
|
.describe("Filter projects with merge requests feature enabled"),
|
|
1097
|
-
min_access_level: z
|
|
1098
|
-
|
|
1099
|
-
.optional()
|
|
1100
|
-
.describe("Filter by minimum access level"),
|
|
1101
|
-
with_programming_language: z
|
|
1102
|
-
.string()
|
|
1103
|
-
.optional()
|
|
1104
|
-
.describe("Filter by programming language"),
|
|
1003
|
+
min_access_level: z.number().optional().describe("Filter by minimum access level"),
|
|
1004
|
+
with_programming_language: z.string().optional().describe("Filter by programming language"),
|
|
1105
1005
|
starred: z.boolean().optional().describe("Filter by starred projects"),
|
|
1106
1006
|
statistics: z.boolean().optional().describe("Include project statistics"),
|
|
1107
|
-
with_custom_attributes: z
|
|
1108
|
-
|
|
1109
|
-
.optional()
|
|
1110
|
-
.describe("Include custom attributes"),
|
|
1111
|
-
with_security_reports: z
|
|
1112
|
-
.boolean()
|
|
1113
|
-
.optional()
|
|
1114
|
-
.describe("Include security reports"),
|
|
1007
|
+
with_custom_attributes: z.boolean().optional().describe("Include custom attributes"),
|
|
1008
|
+
with_security_reports: z.boolean().optional().describe("Include security reports"),
|
|
1115
1009
|
});
|
|
1116
1010
|
// Add wiki operation schemas
|
|
1117
1011
|
export const ListWikiPagesSchema = z.object({
|
|
@@ -1127,20 +1021,14 @@ export const CreateWikiPageSchema = z.object({
|
|
|
1127
1021
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
1128
1022
|
title: z.string().describe("Title of the wiki page"),
|
|
1129
1023
|
content: z.string().describe("Content of the wiki page"),
|
|
1130
|
-
format: z
|
|
1131
|
-
.string()
|
|
1132
|
-
.optional()
|
|
1133
|
-
.describe("Content format, e.g., markdown, rdoc"),
|
|
1024
|
+
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
|
|
1134
1025
|
});
|
|
1135
1026
|
export const UpdateWikiPageSchema = z.object({
|
|
1136
1027
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
1137
1028
|
slug: z.string().describe("URL-encoded slug of the wiki page"),
|
|
1138
1029
|
title: z.string().optional().describe("New title of the wiki page"),
|
|
1139
1030
|
content: z.string().optional().describe("New content of the wiki page"),
|
|
1140
|
-
format: z
|
|
1141
|
-
.string()
|
|
1142
|
-
.optional()
|
|
1143
|
-
.describe("Content format, e.g., markdown, rdoc"),
|
|
1031
|
+
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
|
|
1144
1032
|
});
|
|
1145
1033
|
export const DeleteWikiPageSchema = z.object({
|
|
1146
1034
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
|
@@ -1181,12 +1069,27 @@ export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
|
|
|
1181
1069
|
// Schema for listing project milestones
|
|
1182
1070
|
export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({
|
|
1183
1071
|
iids: z.array(z.number()).optional().describe("Return only the milestones having the given iid"),
|
|
1184
|
-
state: z
|
|
1185
|
-
|
|
1186
|
-
|
|
1072
|
+
state: z
|
|
1073
|
+
.enum(["active", "closed"])
|
|
1074
|
+
.optional()
|
|
1075
|
+
.describe("Return only active or closed milestones"),
|
|
1076
|
+
title: z
|
|
1077
|
+
.string()
|
|
1078
|
+
.optional()
|
|
1079
|
+
.describe("Return only milestones with a title matching the provided string"),
|
|
1080
|
+
search: z
|
|
1081
|
+
.string()
|
|
1082
|
+
.optional()
|
|
1083
|
+
.describe("Return only milestones with a title or description matching the provided string"),
|
|
1187
1084
|
include_ancestors: z.boolean().optional().describe("Include ancestor groups"),
|
|
1188
|
-
updated_before: z
|
|
1189
|
-
|
|
1085
|
+
updated_before: z
|
|
1086
|
+
.string()
|
|
1087
|
+
.optional()
|
|
1088
|
+
.describe("Return milestones updated before the specified date (ISO 8601 format)"),
|
|
1089
|
+
updated_after: z
|
|
1090
|
+
.string()
|
|
1091
|
+
.optional()
|
|
1092
|
+
.describe("Return milestones updated after the specified date (ISO 8601 format)"),
|
|
1190
1093
|
page: z.number().optional().describe("Page number for pagination"),
|
|
1191
1094
|
per_page: z.number().optional().describe("Number of items per page (max 100)"),
|
|
1192
1095
|
});
|
|
@@ -1207,7 +1110,10 @@ export const EditProjectMilestoneSchema = GetProjectMilestoneSchema.extend({
|
|
|
1207
1110
|
description: z.string().optional().describe("The description of the milestone"),
|
|
1208
1111
|
due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"),
|
|
1209
1112
|
start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"),
|
|
1210
|
-
state_event: z
|
|
1113
|
+
state_event: z
|
|
1114
|
+
.enum(["close", "activate"])
|
|
1115
|
+
.optional()
|
|
1116
|
+
.describe("The state event of the milestone"),
|
|
1211
1117
|
});
|
|
1212
1118
|
// Schema for deleting a milestone
|
|
1213
1119
|
export const DeleteProjectMilestoneSchema = GetProjectMilestoneSchema;
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { fileURLToPath } from
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
4
|
const __filename = fileURLToPath(import.meta.url);
|
|
5
5
|
const __dirname = path.dirname(__filename);
|
|
6
6
|
async function main() {
|
|
7
|
-
const repoRoot = path.resolve(__dirname,
|
|
8
|
-
const indexPath = path.join(repoRoot,
|
|
9
|
-
const readmePath = path.join(repoRoot,
|
|
7
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
8
|
+
const indexPath = path.join(repoRoot, "index.ts");
|
|
9
|
+
const readmePath = path.join(repoRoot, "README.md");
|
|
10
10
|
// 1. Read index.ts
|
|
11
|
-
const code = fs.readFileSync(indexPath,
|
|
11
|
+
const code = fs.readFileSync(indexPath, "utf-8");
|
|
12
12
|
// 2. Extract allTools array block
|
|
13
13
|
const match = code.match(/const allTools = \[([\s\S]*?)\];/);
|
|
14
14
|
if (!match) {
|
|
15
|
-
console.error(
|
|
15
|
+
console.error("Unable to locate allTools array in index.ts");
|
|
16
16
|
process.exit(1);
|
|
17
17
|
}
|
|
18
18
|
const toolsBlock = match[1];
|
|
@@ -27,13 +27,13 @@ async function main() {
|
|
|
27
27
|
const lines = tools.map((tool, index) => {
|
|
28
28
|
return `${index + 1}. \`${tool.name}\` - ${tool.description}`;
|
|
29
29
|
});
|
|
30
|
-
const markdown = lines.join(
|
|
30
|
+
const markdown = lines.join("\n");
|
|
31
31
|
// 5. Read README.md and replace between markers
|
|
32
|
-
const readme = fs.readFileSync(readmePath,
|
|
32
|
+
const readme = fs.readFileSync(readmePath, "utf-8");
|
|
33
33
|
const updated = readme.replace(/<!-- TOOLS-START -->([\s\S]*?)<!-- TOOLS-END -->/, `<!-- TOOLS-START -->\n${markdown}\n<!-- TOOLS-END -->`);
|
|
34
34
|
// 6. Write back
|
|
35
|
-
fs.writeFileSync(readmePath, updated,
|
|
36
|
-
console.log(
|
|
35
|
+
fs.writeFileSync(readmePath, updated, "utf-8");
|
|
36
|
+
console.log("README.md tools section updated.");
|
|
37
37
|
}
|
|
38
38
|
main().catch(err => {
|
|
39
39
|
console.error(err);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
// Integration tests that run against real GitLab API (when credentials are provided)
|
|
4
|
+
const GITLAB_API_URL = process.env.GITLAB_API_URL || 'https://gitlab.com';
|
|
5
|
+
const GITLAB_TOKEN = process.env.GITLAB_TOKEN || '';
|
|
6
|
+
const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID || '';
|
|
7
|
+
const skipIntegrationTests = !GITLAB_TOKEN || !TEST_PROJECT_ID;
|
|
8
|
+
describe('GitLab MCP Server Integration Tests', () => {
|
|
9
|
+
if (skipIntegrationTests) {
|
|
10
|
+
it('should skip integration tests when credentials are missing', () => {
|
|
11
|
+
console.log('Skipping integration tests: Missing GITLAB_TOKEN or TEST_PROJECT_ID');
|
|
12
|
+
expect(true).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
let testIssueId = null;
|
|
17
|
+
let testMRId = null;
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
console.log('Running integration tests with GitLab API');
|
|
20
|
+
});
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
// Cleanup: Delete test issue and MR if created
|
|
23
|
+
if (testIssueId) {
|
|
24
|
+
await fetch(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/issues/${testIssueId}`, {
|
|
25
|
+
method: 'DELETE',
|
|
26
|
+
headers: {
|
|
27
|
+
'Authorization': `Bearer ${GITLAB_TOKEN}`
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
describe('Project Access', () => {
|
|
33
|
+
it('should fetch project information', async () => {
|
|
34
|
+
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}`, {
|
|
35
|
+
headers: {
|
|
36
|
+
'Authorization': `Bearer ${GITLAB_TOKEN}`,
|
|
37
|
+
'Accept': 'application/json'
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
expect(response.ok).toBe(true);
|
|
41
|
+
const project = await response.json();
|
|
42
|
+
expect(project).toHaveProperty('id');
|
|
43
|
+
expect(project).toHaveProperty('name');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('Issue Operations', () => {
|
|
47
|
+
it('should create an issue', async () => {
|
|
48
|
+
const issueData = {
|
|
49
|
+
title: `Test Issue ${Date.now()}`,
|
|
50
|
+
description: 'This is a test issue created by MCP integration tests'
|
|
51
|
+
};
|
|
52
|
+
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/issues`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Authorization': `Bearer ${GITLAB_TOKEN}`,
|
|
56
|
+
'Accept': 'application/json',
|
|
57
|
+
'Content-Type': 'application/json'
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify(issueData)
|
|
60
|
+
});
|
|
61
|
+
expect(response.ok).toBe(true);
|
|
62
|
+
const issue = await response.json();
|
|
63
|
+
expect(issue).toHaveProperty('iid');
|
|
64
|
+
expect(issue.title).toBe(issueData.title);
|
|
65
|
+
testIssueId = issue.iid;
|
|
66
|
+
});
|
|
67
|
+
it('should list issues', async () => {
|
|
68
|
+
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/issues?state=opened`, {
|
|
69
|
+
headers: {
|
|
70
|
+
'Authorization': `Bearer ${GITLAB_TOKEN}`,
|
|
71
|
+
'Accept': 'application/json'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
expect(response.ok).toBe(true);
|
|
75
|
+
const issues = await response.json();
|
|
76
|
+
expect(Array.isArray(issues)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it('should add a comment to an issue', async () => {
|
|
79
|
+
if (!testIssueId) {
|
|
80
|
+
console.log('Skipping comment test: No test issue created');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const commentData = {
|
|
84
|
+
body: 'Test comment from MCP integration tests'
|
|
85
|
+
};
|
|
86
|
+
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/issues/${testIssueId}/notes`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'Authorization': `Bearer ${GITLAB_TOKEN}`,
|
|
90
|
+
'Accept': 'application/json',
|
|
91
|
+
'Content-Type': 'application/json'
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify(commentData)
|
|
94
|
+
});
|
|
95
|
+
expect(response.ok).toBe(true);
|
|
96
|
+
const comment = await response.json();
|
|
97
|
+
expect(comment.body).toBe(commentData.body);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('Merge Request Operations', () => {
|
|
101
|
+
it('should list merge requests', async () => {
|
|
102
|
+
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/merge_requests?state=opened`, {
|
|
103
|
+
headers: {
|
|
104
|
+
'Authorization': `Bearer ${GITLAB_TOKEN}`,
|
|
105
|
+
'Accept': 'application/json'
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
expect(response.ok).toBe(true);
|
|
109
|
+
const mrs = await response.json();
|
|
110
|
+
expect(Array.isArray(mrs)).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('Repository Operations', () => {
|
|
114
|
+
it('should fetch repository branches', async () => {
|
|
115
|
+
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches`, {
|
|
116
|
+
headers: {
|
|
117
|
+
'Authorization': `Bearer ${GITLAB_TOKEN}`,
|
|
118
|
+
'Accept': 'application/json'
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
expect(response.ok).toBe(true);
|
|
122
|
+
const branches = await response.json();
|
|
123
|
+
expect(Array.isArray(branches)).toBe(true);
|
|
124
|
+
expect(branches.length).toBeGreaterThan(0);
|
|
125
|
+
});
|
|
126
|
+
it('should fetch repository commits', async () => {
|
|
127
|
+
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/commits?per_page=5`, {
|
|
128
|
+
headers: {
|
|
129
|
+
'Authorization': `Bearer ${GITLAB_TOKEN}`,
|
|
130
|
+
'Accept': 'application/json'
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
expect(response.ok).toBe(true);
|
|
134
|
+
const commits = await response.json();
|
|
135
|
+
expect(Array.isArray(commits)).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('Pipeline Operations', () => {
|
|
139
|
+
it('should list pipelines', async () => {
|
|
140
|
+
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines?per_page=5`, {
|
|
141
|
+
headers: {
|
|
142
|
+
'Authorization': `Bearer ${GITLAB_TOKEN}`,
|
|
143
|
+
'Accept': 'application/json'
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
expect(response.ok).toBe(true);
|
|
147
|
+
const pipelines = await response.json();
|
|
148
|
+
expect(Array.isArray(pipelines)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
// Mock fetch for unit tests
|
|
4
|
+
jest.mock('node-fetch');
|
|
5
|
+
const mockedFetch = fetch;
|
|
6
|
+
describe('GitLab MCP Server Unit Tests', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
describe('API URL Construction', () => {
|
|
14
|
+
it('should use plural resource names in API endpoints', () => {
|
|
15
|
+
const projectId = 'test/project';
|
|
16
|
+
const issueIid = 123;
|
|
17
|
+
// Test issue endpoint
|
|
18
|
+
const issueUrl = `/api/v4/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`;
|
|
19
|
+
expect(issueUrl).toContain('/issues/');
|
|
20
|
+
expect(issueUrl).not.toContain('/issue/');
|
|
21
|
+
// Test merge request endpoint
|
|
22
|
+
const mrUrl = `/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${issueIid}`;
|
|
23
|
+
expect(mrUrl).toContain('/merge_requests/');
|
|
24
|
+
expect(mrUrl).not.toContain('/merge_request/');
|
|
25
|
+
});
|
|
26
|
+
it('should properly encode project IDs with special characters', () => {
|
|
27
|
+
const projectId = 'namespace/project-name';
|
|
28
|
+
const encoded = encodeURIComponent(projectId);
|
|
29
|
+
expect(encoded).toBe('namespace%2Fproject-name');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('API Response Handling', () => {
|
|
33
|
+
it('should handle successful responses', async () => {
|
|
34
|
+
const mockResponse = {
|
|
35
|
+
ok: true,
|
|
36
|
+
status: 200,
|
|
37
|
+
json: async () => ({ id: 1, title: 'Test Issue' })
|
|
38
|
+
};
|
|
39
|
+
mockedFetch.mockResolvedValueOnce(mockResponse);
|
|
40
|
+
const response = await fetch('https://gitlab.com/api/v4/projects/1/issues/1');
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
expect(response.ok).toBe(true);
|
|
43
|
+
expect(data).toEqual({ id: 1, title: 'Test Issue' });
|
|
44
|
+
});
|
|
45
|
+
it('should handle error responses', async () => {
|
|
46
|
+
const mockResponse = {
|
|
47
|
+
ok: false,
|
|
48
|
+
status: 404,
|
|
49
|
+
statusText: 'Not Found',
|
|
50
|
+
text: async () => '{"message":"404 Project Not Found"}'
|
|
51
|
+
};
|
|
52
|
+
mockedFetch.mockResolvedValueOnce(mockResponse);
|
|
53
|
+
const response = await fetch('https://gitlab.com/api/v4/projects/999/issues/1');
|
|
54
|
+
expect(response.ok).toBe(false);
|
|
55
|
+
expect(response.status).toBe(404);
|
|
56
|
+
});
|
|
57
|
+
it('should handle rate limiting', async () => {
|
|
58
|
+
const mockResponse = {
|
|
59
|
+
ok: false,
|
|
60
|
+
status: 429,
|
|
61
|
+
statusText: 'Too Many Requests',
|
|
62
|
+
headers: {
|
|
63
|
+
get: (name) => name === 'RateLimit-Reset' ? '1234567890' : null
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
mockedFetch.mockResolvedValueOnce(mockResponse);
|
|
67
|
+
const response = await fetch('https://gitlab.com/api/v4/projects/1/issues');
|
|
68
|
+
expect(response.status).toBe(429);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('Authentication', () => {
|
|
72
|
+
it('should include Bearer token in Authorization header', () => {
|
|
73
|
+
const token = 'test-token-123';
|
|
74
|
+
const headers = {
|
|
75
|
+
'Authorization': `Bearer ${token}`,
|
|
76
|
+
'Accept': 'application/json',
|
|
77
|
+
'Content-Type': 'application/json'
|
|
78
|
+
};
|
|
79
|
+
expect(headers.Authorization).toBe('Bearer test-token-123');
|
|
80
|
+
});
|
|
81
|
+
it('should handle missing token gracefully', () => {
|
|
82
|
+
const token = process.env.GITLAB_TOKEN || '';
|
|
83
|
+
expect(token).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('Data Validation', () => {
|
|
87
|
+
it('should validate required fields for issue creation', () => {
|
|
88
|
+
const validIssue = {
|
|
89
|
+
title: 'Test Issue',
|
|
90
|
+
description: 'Test Description'
|
|
91
|
+
};
|
|
92
|
+
expect(validIssue.title).toBeTruthy();
|
|
93
|
+
expect(validIssue.title.length).toBeGreaterThan(0);
|
|
94
|
+
});
|
|
95
|
+
it('should validate merge request parameters', () => {
|
|
96
|
+
const validMR = {
|
|
97
|
+
source_branch: 'feature-branch',
|
|
98
|
+
target_branch: 'main',
|
|
99
|
+
title: 'Test MR'
|
|
100
|
+
};
|
|
101
|
+
expect(validMR.source_branch).not.toBe(validMR.target_branch);
|
|
102
|
+
expect(validMR.title).toBeTruthy();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('Error Handling', () => {
|
|
106
|
+
it('should handle network errors', async () => {
|
|
107
|
+
mockedFetch.mockRejectedValueOnce(new Error('Network error'));
|
|
108
|
+
await expect(fetch('https://gitlab.com/api/v4/projects/1/issues'))
|
|
109
|
+
.rejects.toThrow('Network error');
|
|
110
|
+
});
|
|
111
|
+
it('should handle JSON parsing errors', async () => {
|
|
112
|
+
const mockResponse = {
|
|
113
|
+
ok: true,
|
|
114
|
+
status: 200,
|
|
115
|
+
json: async () => { throw new Error('Invalid JSON'); }
|
|
116
|
+
};
|
|
117
|
+
mockedFetch.mockResolvedValueOnce(mockResponse);
|
|
118
|
+
const response = await fetch('https://gitlab.com/api/v4/projects/1/issues');
|
|
119
|
+
await expect(response.json()).rejects.toThrow('Invalid JSON');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.51",
|
|
4
4
|
"description": "MCP server for using the GitLab API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "zereight",
|
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
"prepare": "npm run build",
|
|
21
21
|
"watch": "tsc --watch",
|
|
22
22
|
"deploy": "npm publish --access public",
|
|
23
|
-
"generate-tools": "npx ts-node scripts/generate-tools-readme.ts"
|
|
23
|
+
"generate-tools": "npx ts-node scripts/generate-tools-readme.ts",
|
|
24
|
+
"test": "node test/validate-api.js",
|
|
25
|
+
"test:integration": "node test/validate-api.js",
|
|
26
|
+
"lint": "eslint . --ext .ts",
|
|
27
|
+
"lint:fix": "eslint . --ext .ts --fix",
|
|
28
|
+
"format": "prettier --write \"**/*.{js,ts,json,md}\"",
|
|
29
|
+
"format:check": "prettier --check \"**/*.{js,ts,json,md}\""
|
|
24
30
|
},
|
|
25
31
|
"dependencies": {
|
|
26
32
|
"@modelcontextprotocol/sdk": "1.8.0",
|
|
@@ -35,6 +41,11 @@
|
|
|
35
41
|
"devDependencies": {
|
|
36
42
|
"@types/node": "^22.13.10",
|
|
37
43
|
"typescript": "^5.8.2",
|
|
38
|
-
"zod": "^3.24.2"
|
|
44
|
+
"zod": "^3.24.2",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
|
46
|
+
"@typescript-eslint/parser": "^8.21.0",
|
|
47
|
+
"eslint": "^9.18.0",
|
|
48
|
+
"prettier": "^3.4.2",
|
|
49
|
+
"ts-node": "^10.9.2"
|
|
39
50
|
}
|
|
40
51
|
}
|