@zereight/mcp-gitlab 1.0.72 → 1.0.74

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
@@ -845,12 +845,12 @@ async function listIssues(projectId, options = {}) {
845
845
  url.searchParams.append(`${key}[]`, label.toString());
846
846
  });
847
847
  }
848
- else {
848
+ else if (value) {
849
849
  url.searchParams.append(`${key}[]`, value.toString());
850
850
  }
851
851
  }
852
852
  else {
853
- url.searchParams.append(key, value.toString());
853
+ url.searchParams.append(key, String(value));
854
854
  }
855
855
  }
856
856
  });
@@ -879,7 +879,7 @@ async function listMergeRequests(projectId, options = {}) {
879
879
  url.searchParams.append(key, value.join(","));
880
880
  }
881
881
  else {
882
- url.searchParams.append(key, value.toString());
882
+ url.searchParams.append(key, String(value));
883
883
  }
884
884
  }
885
885
  });
@@ -2617,6 +2617,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2617
2617
  if (GITLAB_AUTH_COOKIE_PATH) {
2618
2618
  await ensureSessionForRequest();
2619
2619
  }
2620
+ logger.info(request.params.name);
2620
2621
  switch (request.params.name) {
2621
2622
  case "fork_repository": {
2622
2623
  if (GITLAB_PROJECT_ID) {
@@ -3305,6 +3306,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3305
3306
  }
3306
3307
  }
3307
3308
  catch (error) {
3309
+ logger.debug(request.params);
3308
3310
  if (error instanceof z.ZodError) {
3309
3311
  throw new Error(`Invalid arguments: ${error.errors
3310
3312
  .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,7 +745,7 @@ 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"),
@@ -794,7 +786,7 @@ export const GetBranchDiffsSchema = ProjectParamsSchema.extend({
794
786
  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
787
  });
796
788
  export const GetMergeRequestSchema = ProjectParamsSchema.extend({
797
- merge_request_iid: z.string().or(z.number().optional().describe("The IID of a merge request")),
789
+ merge_request_iid: z.coerce.string().optional().describe("The IID of a merge request"),
798
790
  source_branch: z.string().optional().describe("Source branch name"),
799
791
  });
800
792
  export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
@@ -830,19 +822,19 @@ export const ListMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
830
822
  unidiff: flexibleBoolean.optional().describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
831
823
  });
832
824
  export const CreateNoteSchema = z.object({
833
- project_id: z.string().describe("Project ID or namespace/project_path"),
825
+ project_id: z.coerce.string().describe("Project ID or namespace/project_path"),
834
826
  noteable_type: z
835
827
  .enum(["issue", "merge_request"])
836
828
  .describe("Type of noteable (issue or merge_request)"),
837
- noteable_iid: z.coerce.number().describe("IID of the issue or merge request"),
829
+ noteable_iid: z.coerce.string().describe("IID of the issue or merge request"),
838
830
  body: z.string().describe("Note content"),
839
831
  });
840
832
  // Issues API operation schemas
841
833
  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")),
834
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
835
+ assignee_id: z.coerce.string().optional().describe("Return issues assigned to the given user ID. user id or none or any"),
844
836
  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")),
837
+ author_id: z.coerce.string().optional().describe("Return issues created by the given user ID"),
846
838
  author_username: z.string().optional().describe("Return issues created by the given username"),
847
839
  confidential: flexibleBoolean.optional().describe("Filter confidential or public issues"),
848
840
  created_after: z.string().optional().describe("Return issues created after the given time"),
@@ -865,24 +857,20 @@ export const ListIssuesSchema = z.object({
865
857
  }).merge(PaginationOptionsSchema);
866
858
  // Merge Requests API operation schemas
867
859
  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"),
860
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
861
+ assignee_id: z.coerce.string().optional().describe("Return issues assigned to the given user ID. user id or none or any"),
873
862
  assignee_username: z
874
863
  .string()
875
864
  .optional()
876
865
  .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")),
866
+ author_id: z.coerce.string().optional().describe("Returns merge requests created by the given user ID"),
878
867
  author_username: z
879
868
  .string()
880
869
  .optional()
881
870
  .describe("Returns merge requests created by the given username"),
882
- reviewer_id: z
883
- .number()
871
+ reviewer_id: z.coerce.string()
884
872
  .optional()
885
- .describe("Returns merge requests which have the user as a reviewer"),
873
+ .describe("Returns merge requests which have the user as a reviewer. user id or none or any"),
886
874
  reviewer_username: z
887
875
  .string()
888
876
  .optional()
@@ -934,12 +922,12 @@ export const ListMergeRequestsSchema = z.object({
934
922
  with_labels_details: flexibleBoolean.optional().describe("Return more details for each label"),
935
923
  }).merge(PaginationOptionsSchema);
936
924
  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")),
925
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
926
+ issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
939
927
  });
940
928
  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")),
929
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
930
+ issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
943
931
  title: z.string().optional().describe("The title of the issue"),
944
932
  description: z.string().optional().describe("The description of the issue"),
945
933
  assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign issue to"),
@@ -947,13 +935,13 @@ export const UpdateIssueSchema = z.object({
947
935
  discussion_locked: flexibleBoolean.optional().describe("Flag to lock discussions"),
948
936
  due_date: z.string().optional().describe("Date the issue is due (YYYY-MM-DD)"),
949
937
  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")),
938
+ milestone_id: z.coerce.string().optional().describe("Milestone ID to assign"),
951
939
  state_event: z.enum(["close", "reopen"]).optional().describe("Update issue state (close/reopen)"),
952
940
  weight: z.number().optional().describe("Weight of the issue (0-9)"),
953
941
  });
954
942
  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")),
943
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
944
+ issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
957
945
  });
958
946
  // Issue links related schemas
959
947
  export const GitLabIssueLinkSchema = z.object({
@@ -962,28 +950,28 @@ export const GitLabIssueLinkSchema = z.object({
962
950
  link_type: z.enum(["relates_to", "blocks", "is_blocked_by"]),
963
951
  });
964
952
  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")),
953
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
954
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
967
955
  });
968
956
  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")),
957
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
958
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
959
+ issue_link_id: z.coerce.string().describe("ID of an issue relationship"),
972
960
  });
973
961
  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")),
962
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
963
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
964
+ target_project_id: z.coerce.string().describe("The ID or URL-encoded path of a target project"),
965
+ target_issue_iid: z.coerce.string().describe("The internal ID of a target project's issue"),
978
966
  link_type: z
979
967
  .enum(["relates_to", "blocks", "is_blocked_by"])
980
968
  .optional()
981
969
  .describe("The type of the relation, defaults to relates_to"),
982
970
  });
983
971
  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")),
972
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
973
+ issue_iid: z.coerce.string().describe("The internal ID of a project's issue"),
974
+ issue_link_id: z.coerce.string().describe("The ID of an issue relationship"),
987
975
  });
988
976
  // Namespace API operation schemas
989
977
  export const ListNamespacesSchema = z.object({
@@ -991,14 +979,14 @@ export const ListNamespacesSchema = z.object({
991
979
  owned: flexibleBoolean.optional().describe("Filter for namespaces owned by current user"),
992
980
  }).merge(PaginationOptionsSchema);
993
981
  export const GetNamespaceSchema = z.object({
994
- namespace_id: z.string().describe("Namespace ID or full path"),
982
+ namespace_id: z.coerce.string().describe("Namespace ID or full path"),
995
983
  });
996
984
  export const VerifyNamespaceSchema = z.object({
997
985
  path: z.string().describe("Namespace path to verify"),
998
986
  });
999
987
  // Project API operation schemas
1000
988
  export const GetProjectSchema = z.object({
1001
- project_id: z.string().describe("Project ID or URL-encoded path"),
989
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1002
990
  });
1003
991
  export const ListProjectsSchema = z.object({
1004
992
  search: z.string().optional().describe("Search term for projects"),
@@ -1031,7 +1019,7 @@ export const ListProjectsSchema = z.object({
1031
1019
  }).merge(PaginationOptionsSchema);
1032
1020
  // Label operation schemas
1033
1021
  export const ListLabelsSchema = z.object({
1034
- project_id: z.string().describe("Project ID or URL-encoded path"),
1022
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1035
1023
  with_counts: z
1036
1024
  .boolean()
1037
1025
  .optional()
@@ -1040,12 +1028,12 @@ export const ListLabelsSchema = z.object({
1040
1028
  search: z.string().optional().describe("Keyword to filter labels by"),
1041
1029
  });
1042
1030
  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"),
1031
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1032
+ label_id: z.coerce.string().describe("The ID or title of a project's label"),
1045
1033
  include_ancestor_groups: flexibleBoolean.optional().describe("Include ancestor groups"),
1046
1034
  });
1047
1035
  export const CreateLabelSchema = z.object({
1048
- project_id: z.string().describe("Project ID or URL-encoded path"),
1036
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1049
1037
  name: z.string().describe("The name of the label"),
1050
1038
  color: z
1051
1039
  .string()
@@ -1054,8 +1042,8 @@ export const CreateLabelSchema = z.object({
1054
1042
  priority: z.number().nullable().optional().describe("The priority of the label"),
1055
1043
  });
1056
1044
  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"),
1045
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1046
+ label_id: z.coerce.string().describe("The ID or title of a project's label"),
1059
1047
  new_name: z.string().optional().describe("The new name of the label"),
1060
1048
  color: z
1061
1049
  .string()
@@ -1065,12 +1053,12 @@ export const UpdateLabelSchema = z.object({
1065
1053
  priority: z.number().nullable().optional().describe("The new priority of the label"),
1066
1054
  });
1067
1055
  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"),
1056
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1057
+ label_id: z.coerce.string().describe("The ID or title of a project's label"),
1070
1058
  });
1071
1059
  // Group projects schema
1072
1060
  export const ListGroupProjectsSchema = z.object({
1073
- group_id: z.string().describe("Group ID or path"),
1061
+ group_id: z.coerce.string().describe("Group ID or path"),
1074
1062
  include_subgroups: flexibleBoolean.optional().describe("Include projects from subgroups"),
1075
1063
  search: z.string().optional().describe("Search term to filter projects"),
1076
1064
  order_by: z
@@ -1100,28 +1088,28 @@ export const ListGroupProjectsSchema = z.object({
1100
1088
  }).merge(PaginationOptionsSchema);
1101
1089
  // Add wiki operation schemas
1102
1090
  export const ListWikiPagesSchema = z.object({
1103
- project_id: z.string().describe("Project ID or URL-encoded path"),
1091
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1104
1092
  with_content: flexibleBoolean.optional().describe("Include content of the wiki pages"),
1105
1093
  }).merge(PaginationOptionsSchema);
1106
1094
  export const GetWikiPageSchema = z.object({
1107
- project_id: z.string().describe("Project ID or URL-encoded path"),
1095
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1108
1096
  slug: z.string().describe("URL-encoded slug of the wiki page"),
1109
1097
  });
1110
1098
  export const CreateWikiPageSchema = z.object({
1111
- project_id: z.string().describe("Project ID or URL-encoded path"),
1099
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1112
1100
  title: z.string().describe("Title of the wiki page"),
1113
1101
  content: z.string().describe("Content of the wiki page"),
1114
1102
  format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
1115
1103
  });
1116
1104
  export const UpdateWikiPageSchema = z.object({
1117
- project_id: z.string().describe("Project ID or URL-encoded path"),
1105
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1118
1106
  slug: z.string().describe("URL-encoded slug of the wiki page"),
1119
1107
  title: z.string().optional().describe("New title of the wiki page"),
1120
1108
  content: z.string().optional().describe("New content of the wiki page"),
1121
1109
  format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
1122
1110
  });
1123
1111
  export const DeleteWikiPageSchema = z.object({
1124
- project_id: z.string().describe("Project ID or URL-encoded path"),
1112
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1125
1113
  slug: z.string().describe("URL-encoded slug of the wiki page"),
1126
1114
  });
1127
1115
  // Define wiki response schemas
@@ -1151,7 +1139,7 @@ export const MergeRequestThreadPositionSchema = z.object({
1151
1139
  });
1152
1140
  // Schema for creating a new merge request thread
1153
1141
  export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
1154
- merge_request_iid: z.string().or(z.number().describe("The IID of a merge request")),
1142
+ merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
1155
1143
  body: z.string().describe("The content of the thread"),
1156
1144
  position: MergeRequestThreadPositionSchema.optional().describe("Position when creating a diff note"),
1157
1145
  created_at: z.string().optional().describe("Date the thread was created at (ISO 8601 format)"),
@@ -1184,7 +1172,7 @@ export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({
1184
1172
  }).merge(PaginationOptionsSchema);
1185
1173
  // Schema for getting a single milestone
1186
1174
  export const GetProjectMilestoneSchema = ProjectParamsSchema.extend({
1187
- milestone_id: z.string().or(z.number().describe("The ID of a project milestone")),
1175
+ milestone_id: z.coerce.string().describe("The ID of a project milestone"),
1188
1176
  });
1189
1177
  // Schema for creating a new milestone
1190
1178
  export const CreateProjectMilestoneSchema = ProjectParamsSchema.extend({
@@ -1216,7 +1204,7 @@ export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema;
1216
1204
  export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
1217
1205
  // Add schemas for commit operations
1218
1206
  export const ListCommitsSchema = z.object({
1219
- project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
1207
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
1220
1208
  ref_name: z.string().optional().describe("The name of a repository branch, tag or revision range, or if not given the default branch"),
1221
1209
  since: z.string().optional().describe("Only commits after or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"),
1222
1210
  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 +1219,11 @@ export const ListCommitsSchema = z.object({
1231
1219
  per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"),
1232
1220
  });
1233
1221
  export const GetCommitSchema = z.object({
1234
- project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
1222
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
1235
1223
  sha: z.string().describe("The commit hash or name of a repository branch or tag"),
1236
1224
  stats: flexibleBoolean.optional().describe("Include commit stats"),
1237
1225
  });
1238
1226
  export const GetCommitDiffSchema = z.object({
1239
- project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
1227
+ project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
1240
1228
  sha: z.string().describe("The commit hash or name of a repository branch or tag"),
1241
1229
  });
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.74",
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
- });