@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 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": "true"
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, } from "@modelcontextprotocol/sdk/types.js";
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((file) => ({
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((action) => ({
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((iid) => {
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((tool) => readOnlyTools.includes(tool.name))
2060
+ ? allTools.filter(tool => readOnlyTools.includes(tool.name))
2048
2061
  : allTools;
2049
2062
  // Toggle wiki tools by USE_GITLAB_WIKI flag
2050
- let tools = USE_GITLAB_WIKI
2063
+ const tools1 = USE_GITLAB_WIKI
2051
2064
  ? tools0
2052
- : tools0.filter((tool) => !wikiToolNames.includes(tool.name));
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((tool) => {
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((f) => ({ path: f.file_path, content: f.content })));
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((e) => `${e.path.join(".")}: ${e.message}`)
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((error) => {
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.object({
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
- }).optional(),
29
- detailed_status: z.object({
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.object({
40
+ illustration: z
41
+ .object({
38
42
  image: z.string().optional(),
39
43
  size: z.string().optional(),
40
44
  title: z.string().optional(),
41
- }).nullable().optional(),
45
+ })
46
+ .nullable()
47
+ .optional(),
42
48
  favicon: z.string().optional(),
43
- }).optional(),
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.object({
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
- }).optional(),
64
- commit: z.object({
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
- }).optional(),
71
- pipeline: z.object({
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
- }).optional(),
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.enum(['running', 'pending', 'finished', 'branches', 'tags']).optional().describe("The scope of pipelines"),
84
- status: z.enum(['created', 'waiting_for_resource', 'preparing', 'pending', 'running', 'success', 'failed', 'canceled', 'skipped', 'manual', 'scheduled']).optional().describe("The status of pipelines"),
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.string().optional().describe("Return pipelines updated after the specified date"),
90
- updated_before: z.string().optional().describe("Return pipelines updated before the specified date"),
91
- order_by: z.enum(['id', 'status', 'ref', 'updated_at', 'user_id']).optional().describe("Order pipelines by"),
92
- sort: z.enum(['asc', 'desc']).optional().describe("Sort pipelines"),
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.enum(['created', 'pending', 'running', 'failed', 'success', 'canceled', 'skipped', 'manual']).optional().describe("The scope of jobs to show"),
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
- .boolean()
272
- .optional()
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
- }).refine(data => data.body !== undefined || data.resolved !== undefined, {
562
- message: "At least one of 'body' or 'resolved' must be provided"
563
- }).refine(data => !(data.body !== undefined && data.resolved !== undefined), {
564
- message: "Only one of 'body' or 'resolved' can be provided, not both"
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
- .number()
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
- .number()
727
- .optional()
728
- .describe("Return issues assigned to the given user ID"),
729
- assignee_username: z
730
- .string()
731
- .optional()
732
- .describe("Return issues assigned to the given username"),
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
- .string()
770
- .optional()
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
- .enum(["yes", "no"])
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
- .array(z.number())
875
- .optional()
876
- .describe("Array of user IDs to assign issue to"),
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.enum(["asc", "desc"]).optional().describe("Return issue discussions sorted in ascending or descending order"),
917
- order_by: z.enum(["created_at", "updated_at"]).optional().describe("Return issue discussions ordered by created_at or updated_at fields"),
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
- .string()
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
- .boolean()
969
- .optional()
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
- .string()
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
- .number()
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
- .boolean()
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.enum(["active", "closed"]).optional().describe("Return only active or closed milestones"),
1185
- title: z.string().optional().describe("Return only milestones with a title matching the provided string"),
1186
- search: z.string().optional().describe("Return only milestones with a title or description matching the provided string"),
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.string().optional().describe("Return milestones updated before the specified date (ISO 8601 format)"),
1189
- updated_after: z.string().optional().describe("Return milestones updated after the specified date (ISO 8601 format)"),
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.enum(["close", "activate"]).optional().describe("The state event of the milestone"),
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 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
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, 'index.ts');
9
- const readmePath = path.join(repoRoot, 'README.md');
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, 'utf-8');
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('Unable to locate allTools array in index.ts');
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('\n');
30
+ const markdown = lines.join("\n");
31
31
  // 5. Read README.md and replace between markers
32
- const readme = fs.readFileSync(readmePath, 'utf-8');
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, 'utf-8');
36
- console.log('README.md tools section updated.');
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.50",
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
  }