@zereight/mcp-gitlab 1.0.72 → 1.0.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { pino } from 'pino';
3
+ const logger = pino({
4
+ level: process.env.LOG_LEVEL || 'info',
5
+ transport: {
6
+ target: 'pino-pretty',
7
+ options: {
8
+ colorize: true,
9
+ levelFirst: true,
10
+ destination: 2,
11
+ },
12
+ },
13
+ });
14
+ export const flexibleBoolean = z.preprocess((val) => {
15
+ if (typeof val === 'string') {
16
+ return val.toLowerCase() === 'true';
17
+ }
18
+ return val;
19
+ }, z.boolean());
package/build/index.js CHANGED
@@ -320,7 +320,7 @@ const allTools = [
320
320
  },
321
321
  {
322
322
  name: "list_issues",
323
- description: "List issues in a GitLab project with filtering options",
323
+ description: "List issues (default: created by current user only; use scope='all' for all accessible issues)",
324
324
  inputSchema: zodToJsonSchema(ListIssuesSchema),
325
325
  },
326
326
  {
@@ -823,17 +823,23 @@ async function createIssue(projectId, options) {
823
823
  return GitLabIssueSchema.parse(data);
824
824
  }
825
825
  /**
826
- * List issues in a GitLab project
826
+ * List issues across all accessible projects or within a specific project
827
827
  * 프로젝트의 이슈 목록 조회
828
828
  *
829
- * @param {string} projectId - The ID or URL-encoded path of the project
829
+ * @param {string} projectId - The ID or URL-encoded path of the project (optional)
830
830
  * @param {Object} options - Options for listing issues
831
831
  * @returns {Promise<GitLabIssue[]>} List of issues
832
832
  */
833
833
  async function listIssues(projectId, options = {}) {
834
- projectId = decodeURIComponent(projectId); // Decode project ID
835
- const effectiveProjectId = getEffectiveProjectId(projectId);
836
- const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
834
+ let url;
835
+ if (projectId) {
836
+ projectId = decodeURIComponent(projectId); // Decode project ID
837
+ const effectiveProjectId = getEffectiveProjectId(projectId);
838
+ url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
839
+ }
840
+ else {
841
+ url = new URL(`${GITLAB_API_URL}/issues`);
842
+ }
837
843
  // Add all query parameters
838
844
  Object.entries(options).forEach(([key, value]) => {
839
845
  if (value !== undefined) {
@@ -845,12 +851,12 @@ async function listIssues(projectId, options = {}) {
845
851
  url.searchParams.append(`${key}[]`, label.toString());
846
852
  });
847
853
  }
848
- else {
854
+ else if (value) {
849
855
  url.searchParams.append(`${key}[]`, value.toString());
850
856
  }
851
857
  }
852
858
  else {
853
- url.searchParams.append(key, value.toString());
859
+ url.searchParams.append(key, String(value));
854
860
  }
855
861
  }
856
862
  });
@@ -879,7 +885,7 @@ async function listMergeRequests(projectId, options = {}) {
879
885
  url.searchParams.append(key, value.join(","));
880
886
  }
881
887
  else {
882
- url.searchParams.append(key, value.toString());
888
+ url.searchParams.append(key, String(value));
883
889
  }
884
890
  }
885
891
  });
@@ -1053,6 +1059,7 @@ async function createMergeRequest(projectId, options) {
1053
1059
  description: options.description,
1054
1060
  source_branch: options.source_branch,
1055
1061
  target_branch: options.target_branch,
1062
+ target_project_id: options.target_project_id,
1056
1063
  assignee_ids: options.assignee_ids,
1057
1064
  reviewer_ids: options.reviewer_ids,
1058
1065
  labels: options.labels?.join(","),
@@ -2617,6 +2624,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2617
2624
  if (GITLAB_AUTH_COOKIE_PATH) {
2618
2625
  await ensureSessionForRequest();
2619
2626
  }
2627
+ logger.info(request.params.name);
2620
2628
  switch (request.params.name) {
2621
2629
  case "fork_repository": {
2622
2630
  if (GITLAB_PROJECT_ID) {
@@ -3305,6 +3313,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3305
3313
  }
3306
3314
  }
3307
3315
  catch (error) {
3316
+ logger.debug(request.params);
3308
3317
  if (error instanceof z.ZodError) {
3309
3318
  throw new Error(`Invalid arguments: ${error.errors
3310
3319
  .map(e => `${e.path.join(".")}: ${e.message}`)
package/build/schemas.js CHANGED
@@ -1,10 +1,5 @@
1
1
  import { z } from "zod";
2
- const flexibleBoolean = z.preprocess((val) => {
3
- if (typeof val === 'string') {
4
- return val.toLowerCase() === 'true';
5
- }
6
- return val;
7
- }, z.boolean());
2
+ import { flexibleBoolean } from "./customSchemas.js";
8
3
  // Base schemas for common types
9
4
  export const GitLabAuthorSchema = z.object({
10
5
  name: z.string(),
@@ -13,8 +8,8 @@ export const GitLabAuthorSchema = z.object({
13
8
  });
14
9
  // Pipeline related schemas
15
10
  export const GitLabPipelineSchema = z.object({
16
- id: z.string().or(z.number()),
17
- project_id: z.string().or(z.number()),
11
+ id: z.coerce.string(),
12
+ project_id: z.coerce.string(),
18
13
  sha: z.string(),
19
14
  ref: z.string(),
20
15
  status: z.string(),
@@ -28,7 +23,7 @@ export const GitLabPipelineSchema = z.object({
28
23
  coverage: z.number().nullable().optional(),
29
24
  user: z
30
25
  .object({
31
- id: z.string().or(z.number()),
26
+ id: z.coerce.string(),
32
27
  name: z.string(),
33
28
  username: z.string(),
34
29
  avatar_url: z.string().nullable().optional(),
@@ -57,7 +52,7 @@ export const GitLabPipelineSchema = z.object({
57
52
  });
58
53
  // Pipeline job related schemas
59
54
  export const GitLabPipelineJobSchema = z.object({
60
- id: z.string().or(z.number()),
55
+ id: z.coerce.string(),
61
56
  status: z.string(),
62
57
  stage: z.string(),
63
58
  name: z.string(),
@@ -70,7 +65,7 @@ export const GitLabPipelineJobSchema = z.object({
70
65
  duration: z.number().nullable().optional(),
71
66
  user: z
72
67
  .object({
73
- id: z.string().or(z.number()),
68
+ id: z.coerce.string(),
74
69
  name: z.string(),
75
70
  username: z.string(),
76
71
  avatar_url: z.string().nullable().optional(),
@@ -87,8 +82,8 @@ export const GitLabPipelineJobSchema = z.object({
87
82
  .optional(),
88
83
  pipeline: z
89
84
  .object({
90
- id: z.string().or(z.number()),
91
- project_id: z.string().or(z.number()),
85
+ id: z.coerce.string(),
86
+ project_id: z.coerce.string(),
92
87
  status: z.string(),
93
88
  ref: z.string(),
94
89
  sha: z.string(),
@@ -104,7 +99,7 @@ export const PaginationOptionsSchema = z.object({
104
99
  });
105
100
  // Schema for listing pipelines
106
101
  export const ListPipelinesSchema = z.object({
107
- project_id: z.string().describe("Project ID or URL-encoded path"),
102
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
108
103
  scope: z
109
104
  .enum(["running", "pending", "finished", "branches", "tags"])
110
105
  .optional()
@@ -145,13 +140,13 @@ export const ListPipelinesSchema = z.object({
145
140
  }).merge(PaginationOptionsSchema);
146
141
  // Schema for getting a specific pipeline
147
142
  export const GetPipelineSchema = z.object({
148
- project_id: z.string().describe("Project ID or URL-encoded path"),
149
- pipeline_id: z.string().or(z.number().describe("The ID of the pipeline")),
143
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
144
+ pipeline_id: z.coerce.string().describe("The ID of the pipeline"),
150
145
  });
151
146
  // Schema for listing jobs in a pipeline
152
147
  export const ListPipelineJobsSchema = z.object({
153
- project_id: z.string().describe("Project ID or URL-encoded path"),
154
- pipeline_id: z.string().or(z.number().describe("The ID of the pipeline")),
148
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
149
+ pipeline_id: z.coerce.string().describe("The ID of the pipeline"),
155
150
  scope: z
156
151
  .enum(["created", "pending", "running", "failed", "success", "canceled", "skipped", "manual"])
157
152
  .optional()
@@ -160,7 +155,7 @@ export const ListPipelineJobsSchema = z.object({
160
155
  }).merge(PaginationOptionsSchema);
161
156
  // Schema for creating a new pipeline
162
157
  export const CreatePipelineSchema = z.object({
163
- project_id: z.string().describe("Project ID or URL-encoded path"),
158
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
164
159
  ref: z.string().describe("The branch or tag to run the pipeline on"),
165
160
  variables: z
166
161
  .array(z.object({
@@ -172,25 +167,22 @@ export const CreatePipelineSchema = z.object({
172
167
  });
173
168
  // Schema for retrying a pipeline
174
169
  export const RetryPipelineSchema = z.object({
175
- project_id: z.string().describe("Project ID or URL-encoded path"),
176
- pipeline_id: z.string().or(z.number().describe("The ID of the pipeline to retry")),
170
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
171
+ pipeline_id: z.coerce.string().describe("The ID of the pipeline to retry"),
177
172
  });
178
173
  // Schema for canceling a pipeline
179
- export const CancelPipelineSchema = z.object({
180
- project_id: z.string().describe("Project ID or URL-encoded path"),
181
- pipeline_id: z.string().or(z.number().describe("The ID of the pipeline to cancel")),
182
- });
174
+ export const CancelPipelineSchema = RetryPipelineSchema;
183
175
  // Schema for the input parameters for pipeline job operations
184
176
  export const GetPipelineJobOutputSchema = z.object({
185
- project_id: z.string().describe("Project ID or URL-encoded path"),
186
- job_id: z.string().or(z.number().describe("The ID of the job")),
177
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
178
+ job_id: z.coerce.string().describe("The ID of the job"),
187
179
  limit: z.number().optional().describe("Maximum number of lines to return from the end of the log (default: 1000)"),
188
180
  offset: z.number().optional().describe("Number of lines to skip from the end of the log (default: 0)"),
189
181
  });
190
182
  // User schemas
191
183
  export const GitLabUserSchema = z.object({
192
184
  username: z.string(), // Changed from login to match GitLab API
193
- id: z.string().or(z.number()),
185
+ id: z.coerce.string(),
194
186
  name: z.string(),
195
187
  avatar_url: z.string().nullable(),
196
188
  web_url: z.string(), // Changed from html_url to match GitLab API
@@ -199,7 +191,7 @@ export const GetUsersSchema = z.object({
199
191
  usernames: z.array(z.string()).describe("Array of usernames to search for"),
200
192
  });
201
193
  export const GitLabUsersResponseSchema = z.record(z.string(), z.object({
202
- id: z.string().or(z.number()),
194
+ id: z.coerce.string(),
203
195
  username: z.string(),
204
196
  name: z.string(),
205
197
  avatar_url: z.string().nullable(),
@@ -208,15 +200,15 @@ export const GitLabUsersResponseSchema = z.record(z.string(), z.object({
208
200
  // Namespace related schemas
209
201
  // Base schema for project-related operations
210
202
  const ProjectParamsSchema = z.object({
211
- project_id: z.string().describe("Project ID or complete URL-encoded path to project"), // Changed from owner/repo to match GitLab API
203
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"), // Changed from owner/repo to match GitLab API
212
204
  });
213
205
  export const GitLabNamespaceSchema = z.object({
214
- id: z.string().or(z.number()),
206
+ id: z.coerce.string(),
215
207
  name: z.string(),
216
208
  path: z.string(),
217
209
  kind: z.enum(["user", "group"]),
218
210
  full_path: z.string(),
219
- parent_id: z.string().or(z.number().nullable()),
211
+ parent_id: z.coerce.string().nullable(),
220
212
  avatar_url: z.string().nullable(),
221
213
  web_url: z.string(),
222
214
  members_count_with_descendants: z.number().optional(),
@@ -237,14 +229,14 @@ export const GitLabNamespaceExistsResponseSchema = z.object({
237
229
  // Repository related schemas
238
230
  export const GitLabOwnerSchema = z.object({
239
231
  username: z.string(), // Changed from login to match GitLab API
240
- id: z.string().or(z.number()),
232
+ id: z.coerce.string(),
241
233
  avatar_url: z.string().nullable(),
242
234
  web_url: z.string(), // Changed from html_url to match GitLab API
243
235
  name: z.string(), // Added as GitLab includes full name
244
236
  state: z.string(), // Added as GitLab includes user state
245
237
  });
246
238
  export const GitLabRepositorySchema = z.object({
247
- id: z.string().or(z.number()),
239
+ id: z.coerce.string(),
248
240
  name: z.string(),
249
241
  path_with_namespace: z.string(),
250
242
  visibility: z.string().optional(),
@@ -259,7 +251,7 @@ export const GitLabRepositorySchema = z.object({
259
251
  default_branch: z.string().optional(),
260
252
  namespace: z
261
253
  .object({
262
- id: z.string().or(z.number()),
254
+ id: z.coerce.string(),
263
255
  name: z.string(),
264
256
  path: z.string(),
265
257
  kind: z.string(),
@@ -306,7 +298,7 @@ export const GitLabRepositorySchema = z.object({
306
298
  shared_runners_enabled: flexibleBoolean.optional(),
307
299
  shared_with_groups: z
308
300
  .array(z.object({
309
- group_id: z.string().or(z.number()),
301
+ group_id: z.coerce.string(),
310
302
  group_name: z.string(),
311
303
  group_full_path: z.string(),
312
304
  group_access_level: z.number(),
@@ -355,7 +347,7 @@ export const GitLabTreeItemSchema = z.object({
355
347
  mode: z.string(),
356
348
  });
357
349
  export const GetRepositoryTreeSchema = z.object({
358
- project_id: z.string().describe("The ID or URL-encoded path of the project"),
350
+ project_id: z.coerce.string().describe("The ID or URL-encoded path of the project"),
359
351
  path: z.string().optional().describe("The path inside the repository"),
360
352
  ref: z
361
353
  .string()
@@ -402,9 +394,9 @@ export const GitLabReferenceSchema = z.object({
402
394
  });
403
395
  // Milestones rest api output schemas
404
396
  export const GitLabMilestonesSchema = z.object({
405
- id: z.string().or(z.number()),
406
- iid: z.string().or(z.number()),
407
- project_id: z.string().or(z.number()),
397
+ id: z.coerce.string(),
398
+ iid: z.coerce.string(),
399
+ project_id: z.coerce.string(),
408
400
  title: z.string(),
409
401
  description: z.string().nullable(),
410
402
  due_date: z.string().nullable(),
@@ -426,7 +418,7 @@ export const CreateIssueOptionsSchema = z.object({
426
418
  title: z.string(),
427
419
  description: z.string().optional(), // Changed from body to match GitLab API
428
420
  assignee_ids: z.array(z.number()).optional(), // Changed from assignees to match GitLab API
429
- milestone_id: z.string().or(z.number().optional()), // Changed from milestone to match GitLab API
421
+ milestone_id: z.coerce.string().optional(), // Changed from milestone to match GitLab API
430
422
  labels: z.array(z.string()).optional(),
431
423
  });
432
424
  export const GitLabDiffSchema = z.object({
@@ -473,7 +465,7 @@ export const GitLabCompareResultSchema = z.object({
473
465
  });
474
466
  // Issue related schemas
475
467
  export const GitLabLabelSchema = z.object({
476
- id: z.string().or(z.number()),
468
+ id: z.coerce.string(),
477
469
  name: z.string(),
478
470
  color: z.string(),
479
471
  text_color: z.string(),
@@ -487,17 +479,17 @@ export const GitLabLabelSchema = z.object({
487
479
  is_project_label: flexibleBoolean.optional(),
488
480
  });
489
481
  export const GitLabMilestoneSchema = z.object({
490
- id: z.string().or(z.number()),
491
- iid: z.string().or(z.number()), // Added to match GitLab API
482
+ id: z.coerce.string(),
483
+ iid: z.coerce.string(), // Added to match GitLab API
492
484
  title: z.string(),
493
485
  description: z.string().nullable().default(""),
494
486
  state: z.string(),
495
487
  web_url: z.string(), // Changed from html_url to match GitLab API
496
488
  });
497
489
  export const GitLabIssueSchema = z.object({
498
- id: z.string().or(z.number()),
499
- iid: z.string().or(z.number()), // Added to match GitLab API
500
- project_id: z.string().or(z.number()), // Added to match GitLab API
490
+ id: z.coerce.string(),
491
+ iid: z.coerce.string(), // Added to match GitLab API
492
+ project_id: z.coerce.string(), // Added to match GitLab API
501
493
  title: z.string(),
502
494
  description: z.string().nullable().default(""), // Changed from body to match GitLab API
503
495
  state: z.string(),
@@ -531,7 +523,7 @@ export const GitLabIssueSchema = z.object({
531
523
  });
532
524
  // NEW SCHEMA: For issue with link details (used in listing issue links)
533
525
  export const GitLabIssueWithLinkDetailsSchema = GitLabIssueSchema.extend({
534
- issue_link_id: z.string().or(z.number()),
526
+ issue_link_id: z.coerce.string(),
535
527
  link_type: z.enum(["relates_to", "blocks", "is_blocked_by"]),
536
528
  link_created_at: z.string(),
537
529
  link_updated_at: z.string(),
@@ -543,7 +535,7 @@ export const GitLabForkParentSchema = z.object({
543
535
  owner: z
544
536
  .object({
545
537
  username: z.string(), // Changed from login to match GitLab API
546
- id: z.string().or(z.number()),
538
+ id: z.coerce.string(),
547
539
  avatar_url: z.string().nullable(),
548
540
  })
549
541
  .optional(), // Made optional to handle cases where GitLab API doesn't include it
@@ -559,9 +551,9 @@ export const GitLabMergeRequestDiffRefSchema = z.object({
559
551
  start_sha: z.string(),
560
552
  });
561
553
  export const GitLabMergeRequestSchema = z.object({
562
- id: z.string().or(z.number()),
563
- iid: z.string().or(z.number()),
564
- project_id: z.string().or(z.number()),
554
+ id: z.coerce.string(),
555
+ iid: z.coerce.string(),
556
+ project_id: z.coerce.string(),
565
557
  title: z.string(),
566
558
  description: z.string().nullable(),
567
559
  state: z.string(),
@@ -609,7 +601,7 @@ export const LineRangeSchema = z.object({
609
601
  }).describe("Line range for multiline comments on GitLab merge request diffs. VALIDATION RULES: 1) line_code is critical for GitLab API success, 2) start/end must have consistent types, 3) line numbers must form valid range, 4) get line_code from GitLab diff API, never generate manually.");
610
602
  // Discussion related schemas
611
603
  export const GitLabDiscussionNoteSchema = z.object({
612
- id: z.string().or(z.number()),
604
+ id: z.coerce.string(),
613
605
  type: z.enum(["DiscussionNote", "DiffNote", "Note"]).nullable(), // Allow null type for regular notes
614
606
  body: z.string(),
615
607
  attachment: z.any().nullable(), // Can be string or object, handle appropriately
@@ -617,10 +609,10 @@ export const GitLabDiscussionNoteSchema = z.object({
617
609
  created_at: z.string(),
618
610
  updated_at: z.string(),
619
611
  system: flexibleBoolean,
620
- noteable_id: z.string().or(z.number()),
612
+ noteable_id: z.coerce.string(),
621
613
  noteable_type: z.enum(["Issue", "MergeRequest", "Snippet", "Commit", "Epic"]),
622
- project_id: z.string().or(z.number().optional()), // Optional for group-level discussions like Epics
623
- noteable_iid: z.coerce.number().nullable(),
614
+ project_id: z.coerce.string().optional(),
615
+ noteable_iid: z.coerce.string().nullable().optional(),
624
616
  resolvable: flexibleBoolean.optional(),
625
617
  resolved: flexibleBoolean.optional(),
626
618
  resolved_by: GitLabUserSchema.nullable().optional(),
@@ -660,7 +652,7 @@ export const PaginatedResponseSchema = z.object({
660
652
  pagination: GitLabPaginationSchema.optional(),
661
653
  });
662
654
  export const GitLabDiscussionSchema = z.object({
663
- id: z.string(),
655
+ id: z.coerce.string(),
664
656
  individual_note: flexibleBoolean,
665
657
  notes: z.array(GitLabDiscussionNoteSchema),
666
658
  });
@@ -670,18 +662,18 @@ export const PaginatedDiscussionsResponseSchema = z.object({
670
662
  pagination: GitLabPaginationSchema,
671
663
  });
672
664
  export const ListIssueDiscussionsSchema = z.object({
673
- project_id: z.string().describe("Project ID or URL-encoded path"),
674
- issue_iid: z.string().or(z.number().describe("The internal ID of the project issue")),
665
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
666
+ issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
675
667
  }).merge(PaginationOptionsSchema);
676
668
  // Input schema for listing merge request discussions
677
669
  export const ListMergeRequestDiscussionsSchema = ProjectParamsSchema.extend({
678
- merge_request_iid: z.string().or(z.number().describe("The IID of a merge request")),
670
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
679
671
  }).merge(PaginationOptionsSchema);
680
672
  // Input schema for updating a merge request discussion note
681
673
  export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({
682
- merge_request_iid: z.string().or(z.number().describe("The IID of a merge request")),
683
- discussion_id: z.string().describe("The ID of a thread"),
684
- note_id: z.string().or(z.number().describe("The ID of a thread note")),
674
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
675
+ discussion_id: z.coerce.string().describe("The ID of a thread"),
676
+ note_id: z.coerce.string().describe("The ID of a thread note"),
685
677
  body: z.string().optional().describe("The content of the note or reply"),
686
678
  resolved: flexibleBoolean.optional().describe("Resolve or unresolve the note"),
687
679
  })
@@ -693,22 +685,22 @@ export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({
693
685
  });
694
686
  // Input schema for adding a note to an existing merge request discussion
695
687
  export const CreateMergeRequestNoteSchema = ProjectParamsSchema.extend({
696
- merge_request_iid: z.string().or(z.number().describe("The IID of a merge request")),
697
- discussion_id: z.string().describe("The ID of a thread"),
688
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
689
+ discussion_id: z.coerce.string().describe("The ID of a thread"),
698
690
  body: z.string().describe("The content of the note or reply"),
699
691
  created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"),
700
692
  });
701
693
  // Input schema for updating an issue discussion note
702
694
  export const UpdateIssueNoteSchema = ProjectParamsSchema.extend({
703
- issue_iid: z.string().or(z.number().describe("The IID of an issue")),
704
- discussion_id: z.string().describe("The ID of a thread"),
705
- note_id: z.string().or(z.number().describe("The ID of a thread note")),
695
+ issue_iid: z.coerce.string().describe("The IID of an issue"),
696
+ discussion_id: z.coerce.string().describe("The ID of a thread"),
697
+ note_id: z.coerce.string().describe("The ID of a thread note"),
706
698
  body: z.string().describe("The content of the note or reply"),
707
699
  });
708
700
  // Input schema for adding a note to an existing issue discussion
709
701
  export const CreateIssueNoteSchema = ProjectParamsSchema.extend({
710
- issue_iid: z.string().or(z.number().describe("The IID of an issue")),
711
- discussion_id: z.string().describe("The ID of a thread"),
702
+ issue_iid: z.coerce.string().describe("The IID of an issue"),
703
+ discussion_id: z.coerce.string().describe("The ID of a thread"),
712
704
  body: z.string().describe("The content of the note or reply"),
713
705
  created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"),
714
706
  });
@@ -753,13 +745,14 @@ export const CreateIssueSchema = ProjectParamsSchema.extend({
753
745
  description: z.string().optional().describe("Issue description"),
754
746
  assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"),
755
747
  labels: z.array(z.string()).optional().describe("Array of label names"),
756
- milestone_id: z.string().or(z.number().optional().describe("Milestone ID to assign")),
748
+ milestone_id: z.coerce.string().optional().describe("Milestone ID to assign"),
757
749
  });
758
750
  const MergeRequestOptionsSchema = {
759
751
  title: z.string().describe("Merge request title"),
760
752
  description: z.string().optional().describe("Merge request description"),
761
753
  source_branch: z.string().describe("Branch containing changes"),
762
754
  target_branch: z.string().describe("Branch to merge into"),
755
+ target_project_id: z.coerce.string().optional().describe("Numeric ID of the target project."),
763
756
  assignee_ids: z
764
757
  .array(z.number())
765
758
  .optional()
@@ -794,7 +787,7 @@ export const GetBranchDiffsSchema = ProjectParamsSchema.extend({
794
787
  excluded_file_patterns: z.array(z.string()).optional().describe("Array of regex patterns to exclude files from the diff results. Each pattern is a JavaScript-compatible regular expression that matches file paths to ignore. Examples: [\"^test/mocks/\", \"\\.spec\\.ts$\", \"package-lock\\.json\"]"),
795
788
  });
796
789
  export const GetMergeRequestSchema = ProjectParamsSchema.extend({
797
- merge_request_iid: z.string().or(z.number().optional().describe("The IID of a merge request")),
790
+ merge_request_iid: z.coerce.string().optional().describe("The IID of a merge request"),
798
791
  source_branch: z.string().optional().describe("Source branch name"),
799
792
  });
800
793
  export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
@@ -830,19 +823,19 @@ export const ListMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
830
823
  unidiff: flexibleBoolean.optional().describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
831
824
  });
832
825
  export const CreateNoteSchema = z.object({
833
- project_id: z.string().describe("Project ID or namespace/project_path"),
826
+ project_id: z.coerce.string().describe("Project ID or namespace/project_path"),
834
827
  noteable_type: z
835
828
  .enum(["issue", "merge_request"])
836
829
  .describe("Type of noteable (issue or merge_request)"),
837
- noteable_iid: z.coerce.number().describe("IID of the issue or merge request"),
830
+ noteable_iid: z.coerce.string().describe("IID of the issue or merge request"),
838
831
  body: z.string().describe("Note content"),
839
832
  });
840
833
  // Issues API operation schemas
841
834
  export const ListIssuesSchema = z.object({
842
- project_id: z.string().describe("Project ID or URL-encoded path"),
843
- assignee_id: z.string().or(z.number().optional().describe("Return issues assigned to the given user ID")),
835
+ project_id: z.coerce.string().optional().describe("Project ID or URL-encoded path (optional - if not provided, lists issues across all accessible projects)"),
836
+ assignee_id: z.coerce.string().optional().describe("Return issues assigned to the given user ID. user id or none or any"),
844
837
  assignee_username: z.array(z.string()).optional().describe("Return issues assigned to the given username"),
845
- author_id: z.string().or(z.number().optional().describe("Return issues created by the given user ID")),
838
+ author_id: z.coerce.string().optional().describe("Return issues created by the given user ID"),
846
839
  author_username: z.string().optional().describe("Return issues created by the given username"),
847
840
  confidential: flexibleBoolean.optional().describe("Filter confidential or public issues"),
848
841
  created_after: z.string().optional().describe("Return issues created after the given time"),
@@ -865,24 +858,20 @@ export const ListIssuesSchema = z.object({
865
858
  }).merge(PaginationOptionsSchema);
866
859
  // Merge Requests API operation schemas
867
860
  export const ListMergeRequestsSchema = z.object({
868
- project_id: z.string().describe("Project ID or URL-encoded path"),
869
- assignee_id: z
870
- .number()
871
- .optional()
872
- .describe("Returns merge requests assigned to the given user ID"),
861
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
862
+ assignee_id: z.coerce.string().optional().describe("Return issues assigned to the given user ID. user id or none or any"),
873
863
  assignee_username: z
874
864
  .string()
875
865
  .optional()
876
866
  .describe("Returns merge requests assigned to the given username"),
877
- author_id: z.string().or(z.number().optional().describe("Returns merge requests created by the given user ID")),
867
+ author_id: z.coerce.string().optional().describe("Returns merge requests created by the given user ID"),
878
868
  author_username: z
879
869
  .string()
880
870
  .optional()
881
871
  .describe("Returns merge requests created by the given username"),
882
- reviewer_id: z
883
- .number()
872
+ reviewer_id: z.coerce.string()
884
873
  .optional()
885
- .describe("Returns merge requests which have the user as a reviewer"),
874
+ .describe("Returns merge requests which have the user as a reviewer. user id or none or any"),
886
875
  reviewer_username: z
887
876
  .string()
888
877
  .optional()
@@ -934,12 +923,12 @@ export const ListMergeRequestsSchema = z.object({
934
923
  with_labels_details: flexibleBoolean.optional().describe("Return more details for each label"),
935
924
  }).merge(PaginationOptionsSchema);
936
925
  export const GetIssueSchema = z.object({
937
- project_id: z.string().describe("Project ID or URL-encoded path"),
938
- issue_iid: z.string().or(z.number().describe("The internal ID of the project issue")),
926
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
927
+ issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
939
928
  });
940
929
  export const UpdateIssueSchema = z.object({
941
- project_id: z.string().describe("Project ID or URL-encoded path"),
942
- issue_iid: z.string().or(z.number().describe("The internal ID of the project issue")),
930
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
931
+ issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
943
932
  title: z.string().optional().describe("The title of the issue"),
944
933
  description: z.string().optional().describe("The description of the issue"),
945
934
  assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign issue to"),
@@ -947,13 +936,13 @@ export const UpdateIssueSchema = z.object({
947
936
  discussion_locked: flexibleBoolean.optional().describe("Flag to lock discussions"),
948
937
  due_date: z.string().optional().describe("Date the issue is due (YYYY-MM-DD)"),
949
938
  labels: z.array(z.string()).optional().describe("Array of label names"),
950
- milestone_id: z.string().or(z.number().optional().describe("Milestone ID to assign")),
939
+ milestone_id: z.coerce.string().optional().describe("Milestone ID to assign"),
951
940
  state_event: z.enum(["close", "reopen"]).optional().describe("Update issue state (close/reopen)"),
952
941
  weight: z.number().optional().describe("Weight of the issue (0-9)"),
953
942
  });
954
943
  export const DeleteIssueSchema = z.object({
955
- project_id: z.string().describe("Project ID or URL-encoded path"),
956
- issue_iid: z.string().or(z.number().describe("The internal ID of the project issue")),
944
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
945
+ issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
957
946
  });
958
947
  // Issue links related schemas
959
948
  export const GitLabIssueLinkSchema = z.object({
@@ -962,28 +951,28 @@ export const GitLabIssueLinkSchema = z.object({
962
951
  link_type: z.enum(["relates_to", "blocks", "is_blocked_by"]),
963
952
  });
964
953
  export const ListIssueLinksSchema = z.object({
965
- project_id: z.string().describe("Project ID or URL-encoded path"),
966
- issue_iid: z.string().or(z.number().describe("The internal ID of a project's issue")),
954
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
955
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
967
956
  });
968
957
  export const GetIssueLinkSchema = z.object({
969
- project_id: z.string().describe("Project ID or URL-encoded path"),
970
- issue_iid: z.string().or(z.number().describe("The internal ID of a project's issue")),
971
- issue_link_id: z.string().or(z.number().describe("ID of an issue relationship")),
958
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
959
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
960
+ issue_link_id: z.coerce.string().describe("ID of an issue relationship"),
972
961
  });
973
962
  export const CreateIssueLinkSchema = z.object({
974
- project_id: z.string().describe("Project ID or URL-encoded path"),
975
- issue_iid: z.string().or(z.number().describe("The internal ID of a project's issue")),
976
- target_project_id: z.string().describe("The ID or URL-encoded path of a target project"),
977
- target_issue_iid: z.string().or(z.number().describe("The internal ID of a target project's issue")),
963
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
964
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
965
+ target_project_id: z.coerce.string().describe("The ID or URL-encoded path of a target project"),
966
+ target_issue_iid: z.coerce.string().describe("The internal ID of a target project's issue"),
978
967
  link_type: z
979
968
  .enum(["relates_to", "blocks", "is_blocked_by"])
980
969
  .optional()
981
970
  .describe("The type of the relation, defaults to relates_to"),
982
971
  });
983
972
  export const DeleteIssueLinkSchema = z.object({
984
- project_id: z.string().describe("Project ID or URL-encoded path"),
985
- issue_iid: z.string().or(z.number().describe("The internal ID of a project's issue")),
986
- issue_link_id: z.string().or(z.number().describe("The ID of an issue relationship")),
973
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
974
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
975
+ issue_link_id: z.coerce.string().describe("The ID of an issue relationship"),
987
976
  });
988
977
  // Namespace API operation schemas
989
978
  export const ListNamespacesSchema = z.object({
@@ -991,14 +980,14 @@ export const ListNamespacesSchema = z.object({
991
980
  owned: flexibleBoolean.optional().describe("Filter for namespaces owned by current user"),
992
981
  }).merge(PaginationOptionsSchema);
993
982
  export const GetNamespaceSchema = z.object({
994
- namespace_id: z.string().describe("Namespace ID or full path"),
983
+ namespace_id: z.coerce.string().describe("Namespace ID or full path"),
995
984
  });
996
985
  export const VerifyNamespaceSchema = z.object({
997
986
  path: z.string().describe("Namespace path to verify"),
998
987
  });
999
988
  // Project API operation schemas
1000
989
  export const GetProjectSchema = z.object({
1001
- project_id: z.string().describe("Project ID or URL-encoded path"),
990
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1002
991
  });
1003
992
  export const ListProjectsSchema = z.object({
1004
993
  search: z.string().optional().describe("Search term for projects"),
@@ -1031,7 +1020,7 @@ export const ListProjectsSchema = z.object({
1031
1020
  }).merge(PaginationOptionsSchema);
1032
1021
  // Label operation schemas
1033
1022
  export const ListLabelsSchema = z.object({
1034
- project_id: z.string().describe("Project ID or URL-encoded path"),
1023
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1035
1024
  with_counts: z
1036
1025
  .boolean()
1037
1026
  .optional()
@@ -1040,12 +1029,12 @@ export const ListLabelsSchema = z.object({
1040
1029
  search: z.string().optional().describe("Keyword to filter labels by"),
1041
1030
  });
1042
1031
  export const GetLabelSchema = z.object({
1043
- project_id: z.string().describe("Project ID or URL-encoded path"),
1044
- label_id: z.string().describe("The ID or title of a project's label"),
1032
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1033
+ label_id: z.coerce.string().describe("The ID or title of a project's label"),
1045
1034
  include_ancestor_groups: flexibleBoolean.optional().describe("Include ancestor groups"),
1046
1035
  });
1047
1036
  export const CreateLabelSchema = z.object({
1048
- project_id: z.string().describe("Project ID or URL-encoded path"),
1037
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1049
1038
  name: z.string().describe("The name of the label"),
1050
1039
  color: z
1051
1040
  .string()
@@ -1054,8 +1043,8 @@ export const CreateLabelSchema = z.object({
1054
1043
  priority: z.number().nullable().optional().describe("The priority of the label"),
1055
1044
  });
1056
1045
  export const UpdateLabelSchema = z.object({
1057
- project_id: z.string().describe("Project ID or URL-encoded path"),
1058
- label_id: z.string().describe("The ID or title of a project's label"),
1046
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1047
+ label_id: z.coerce.string().describe("The ID or title of a project's label"),
1059
1048
  new_name: z.string().optional().describe("The new name of the label"),
1060
1049
  color: z
1061
1050
  .string()
@@ -1065,12 +1054,12 @@ export const UpdateLabelSchema = z.object({
1065
1054
  priority: z.number().nullable().optional().describe("The new priority of the label"),
1066
1055
  });
1067
1056
  export const DeleteLabelSchema = z.object({
1068
- project_id: z.string().describe("Project ID or URL-encoded path"),
1069
- label_id: z.string().describe("The ID or title of a project's label"),
1057
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1058
+ label_id: z.coerce.string().describe("The ID or title of a project's label"),
1070
1059
  });
1071
1060
  // Group projects schema
1072
1061
  export const ListGroupProjectsSchema = z.object({
1073
- group_id: z.string().describe("Group ID or path"),
1062
+ group_id: z.coerce.string().describe("Group ID or path"),
1074
1063
  include_subgroups: flexibleBoolean.optional().describe("Include projects from subgroups"),
1075
1064
  search: z.string().optional().describe("Search term to filter projects"),
1076
1065
  order_by: z
@@ -1100,28 +1089,28 @@ export const ListGroupProjectsSchema = z.object({
1100
1089
  }).merge(PaginationOptionsSchema);
1101
1090
  // Add wiki operation schemas
1102
1091
  export const ListWikiPagesSchema = z.object({
1103
- project_id: z.string().describe("Project ID or URL-encoded path"),
1092
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1104
1093
  with_content: flexibleBoolean.optional().describe("Include content of the wiki pages"),
1105
1094
  }).merge(PaginationOptionsSchema);
1106
1095
  export const GetWikiPageSchema = z.object({
1107
- project_id: z.string().describe("Project ID or URL-encoded path"),
1096
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1108
1097
  slug: z.string().describe("URL-encoded slug of the wiki page"),
1109
1098
  });
1110
1099
  export const CreateWikiPageSchema = z.object({
1111
- project_id: z.string().describe("Project ID or URL-encoded path"),
1100
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1112
1101
  title: z.string().describe("Title of the wiki page"),
1113
1102
  content: z.string().describe("Content of the wiki page"),
1114
1103
  format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
1115
1104
  });
1116
1105
  export const UpdateWikiPageSchema = z.object({
1117
- project_id: z.string().describe("Project ID or URL-encoded path"),
1106
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1118
1107
  slug: z.string().describe("URL-encoded slug of the wiki page"),
1119
1108
  title: z.string().optional().describe("New title of the wiki page"),
1120
1109
  content: z.string().optional().describe("New content of the wiki page"),
1121
1110
  format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
1122
1111
  });
1123
1112
  export const DeleteWikiPageSchema = z.object({
1124
- project_id: z.string().describe("Project ID or URL-encoded path"),
1113
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1125
1114
  slug: z.string().describe("URL-encoded slug of the wiki page"),
1126
1115
  });
1127
1116
  // Define wiki response schemas
@@ -1151,7 +1140,7 @@ export const MergeRequestThreadPositionSchema = z.object({
1151
1140
  });
1152
1141
  // Schema for creating a new merge request thread
1153
1142
  export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
1154
- merge_request_iid: z.string().or(z.number().describe("The IID of a merge request")),
1143
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
1155
1144
  body: z.string().describe("The content of the thread"),
1156
1145
  position: MergeRequestThreadPositionSchema.optional().describe("Position when creating a diff note"),
1157
1146
  created_at: z.string().optional().describe("Date the thread was created at (ISO 8601 format)"),
@@ -1184,7 +1173,7 @@ export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({
1184
1173
  }).merge(PaginationOptionsSchema);
1185
1174
  // Schema for getting a single milestone
1186
1175
  export const GetProjectMilestoneSchema = ProjectParamsSchema.extend({
1187
- milestone_id: z.string().or(z.number().describe("The ID of a project milestone")),
1176
+ milestone_id: z.coerce.string().describe("The ID of a project milestone"),
1188
1177
  });
1189
1178
  // Schema for creating a new milestone
1190
1179
  export const CreateProjectMilestoneSchema = ProjectParamsSchema.extend({
@@ -1216,7 +1205,7 @@ export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema;
1216
1205
  export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
1217
1206
  // Add schemas for commit operations
1218
1207
  export const ListCommitsSchema = z.object({
1219
- project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
1208
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
1220
1209
  ref_name: z.string().optional().describe("The name of a repository branch, tag or revision range, or if not given the default branch"),
1221
1210
  since: z.string().optional().describe("Only commits after or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"),
1222
1211
  until: z.string().optional().describe("Only commits before or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"),
@@ -1231,11 +1220,11 @@ export const ListCommitsSchema = z.object({
1231
1220
  per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"),
1232
1221
  });
1233
1222
  export const GetCommitSchema = z.object({
1234
- project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
1223
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
1235
1224
  sha: z.string().describe("The commit hash or name of a repository branch or tag"),
1236
1225
  stats: flexibleBoolean.optional().describe("Include commit stats"),
1237
1226
  });
1238
1227
  export const GetCommitDiffSchema = z.object({
1239
- project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
1228
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
1240
1229
  sha: z.string().describe("The commit hash or name of a repository branch or tag"),
1241
1230
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "1.0.72",
3
+ "version": "1.0.75",
4
4
  "description": "MCP server for using the GitLab API",
5
5
  "license": "MIT",
6
6
  "author": "zereight",
@@ -1,151 +0,0 @@
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
- });
@@ -1,122 +0,0 @@
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
- });