@ttpears/gitlab-mcp-server 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js CHANGED
@@ -81,10 +81,15 @@ const getProjectsTool = {
81
81
  inputSchema: withUserAuth(z.object({
82
82
  first: z.number().min(1).max(100).default(20).describe('Number of projects to retrieve'),
83
83
  after: z.string().optional().describe('Cursor for pagination'),
84
+ sort: z.string().optional().describe('Sort order (e.g., UPDATED_DESC, CREATED_DESC, CREATED_ASC). Defaults to UPDATED_DESC for recency.'),
85
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
84
86
  })),
85
87
  handler: async (input, client, userConfig) => {
86
88
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
87
- const result = await client.getProjects(input.first, input.after, credentials);
89
+ const result = await client.getProjects(input.first, input.after, input.fetchAll, credentials, input.sort);
90
+ if (input.fetchAll) {
91
+ return result;
92
+ }
88
93
  return result.projects;
89
94
  },
90
95
  };
@@ -103,10 +108,15 @@ const getIssuesTool = {
103
108
  projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
104
109
  first: z.number().min(1).max(100).default(20).describe('Number of issues to retrieve'),
105
110
  after: z.string().optional().describe('Cursor for pagination'),
111
+ sort: z.string().optional().describe('Sort order (e.g., UPDATED_DESC, CREATED_DESC, CREATED_ASC). Defaults to UPDATED_DESC for recency.'),
112
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
106
113
  })),
107
114
  handler: async (input, client, userConfig) => {
108
115
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
109
- const result = await client.getIssues(input.projectPath, input.first, input.after, credentials);
116
+ const result = await client.getIssues(input.projectPath, input.first, input.after, input.fetchAll, credentials, input.sort);
117
+ if (input.fetchAll) {
118
+ return result;
119
+ }
110
120
  if (!result || !result.project || !result.project.issues) {
111
121
  throw new Error('Project not found or issues are not accessible for the provided path');
112
122
  }
@@ -128,10 +138,15 @@ const getMergeRequestsTool = {
128
138
  projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
129
139
  first: z.number().min(1).max(100).default(20).describe('Number of merge requests to retrieve'),
130
140
  after: z.string().optional().describe('Cursor for pagination'),
141
+ sort: z.string().optional().describe('Sort order (e.g., UPDATED_DESC, CREATED_DESC, CREATED_ASC). Defaults to UPDATED_DESC for recency.'),
142
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
131
143
  })),
132
144
  handler: async (input, client, userConfig) => {
133
145
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
134
- const result = await client.getMergeRequests(input.projectPath, input.first, input.after, credentials);
146
+ const result = await client.getMergeRequests(input.projectPath, input.first, input.after, input.fetchAll, credentials, input.sort);
147
+ if (input.fetchAll) {
148
+ return result;
149
+ }
135
150
  if (!result || !result.project || !result.project.mergeRequests) {
136
151
  throw new Error('Project not found or merge requests are not accessible for the provided path');
137
152
  }
@@ -245,21 +260,6 @@ const getAvailableQueriesTools = {
245
260
  };
246
261
  },
247
262
  };
248
- export const readOnlyTools = [
249
- getProjectTool,
250
- getIssuesTool,
251
- getMergeRequestsTool,
252
- executeCustomQueryTool,
253
- getAvailableQueriesTools,
254
- ];
255
- export const userAuthTools = [
256
- getCurrentUserTool,
257
- getProjectsTool,
258
- ];
259
- export const writeTools = [
260
- createIssueTool,
261
- createMergeRequestTool,
262
- ];
263
263
  const updateIssueTool = {
264
264
  name: 'update_issue',
265
265
  title: 'Update Issue',
@@ -410,15 +410,27 @@ const globalSearchTool = {
410
410
  },
411
411
  inputSchema: withUserAuth(z.object({
412
412
  searchTerm: z.string().optional().transform(val => val?.trim() || undefined).describe('Search term (leave empty for recent activity)'),
413
+ first: z.number().min(1).max(100).default(20).describe('Number of results to retrieve per category'),
414
+ after: z.string().optional().describe('Cursor for pagination'),
415
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results per category'),
413
416
  })),
414
417
  handler: async (input, client, userConfig) => {
415
418
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
416
- const result = await client.globalSearch(input.searchTerm, undefined, credentials);
419
+ if (input.fetchAll) {
420
+ const result = await client.globalSearchAll(input.searchTerm, undefined, credentials);
421
+ return {
422
+ searchTerm: input.searchTerm,
423
+ projects: result.projects,
424
+ issues: result.issues,
425
+ totalResults: result.projects.totalFetched + result.issues.totalFetched,
426
+ };
427
+ }
428
+ const result = await client.globalSearch(input.searchTerm, input.first, input.after, credentials);
417
429
  return {
418
430
  searchTerm: input.searchTerm,
419
- projects: result.projects.nodes,
420
- issues: result.issues.nodes,
421
- totalResults: result.projects.nodes.length + result.issues.nodes.length,
431
+ projects: result.projects,
432
+ issues: result.issues,
433
+ totalResults: (result.projects.nodes?.length || 0) + (result.issues.nodes?.length || 0),
422
434
  _note: 'This is a text search only. For filtering by assignee/author/labels, use search_issues or get_user_issues. For MRs, use search_merge_requests with username.'
423
435
  };
424
436
  },
@@ -441,10 +453,14 @@ const searchProjectsTool = {
441
453
  .describe('Search term to find projects by name or description'),
442
454
  first: z.number().min(1).max(100).default(20).describe('Number of projects to retrieve'),
443
455
  after: z.string().optional().describe('Cursor for pagination'),
456
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
444
457
  })),
445
458
  handler: async (input, client, userConfig) => {
446
459
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
447
- const result = await client.searchProjects(input.searchTerm, input.first, input.after, credentials);
460
+ const result = await client.searchProjects(input.searchTerm, input.first, input.after, input.fetchAll, credentials);
461
+ if (input.fetchAll) {
462
+ return result;
463
+ }
448
464
  return result.projects;
449
465
  },
450
466
  };
@@ -468,10 +484,15 @@ const searchIssuesTool = {
468
484
  labelNames: z.array(z.string()).optional().describe('Filter by label names (e.g., ["Priority::High", "bug"])'),
469
485
  first: z.number().min(1).max(100).default(20).describe('Number of issues to retrieve'),
470
486
  after: z.string().optional().describe('Cursor for pagination'),
487
+ sort: z.string().optional().describe('Sort order (e.g., UPDATED_DESC, CREATED_DESC, CREATED_ASC). Defaults to UPDATED_DESC for recency.'),
488
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
471
489
  })),
472
490
  handler: async (input, client, userConfig) => {
473
491
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
474
- const result = await client.searchIssues(input.searchTerm, input.projectPath, input.state, input.first, input.after, credentials, input.assigneeUsernames, input.authorUsername, input.labelNames);
492
+ const result = await client.searchIssues(input.searchTerm, input.projectPath, input.state, input.first, input.after, input.fetchAll, credentials, input.assigneeUsernames, input.authorUsername, input.labelNames, input.sort);
493
+ if (input.fetchAll) {
494
+ return result;
495
+ }
475
496
  // Return the issues from either project-specific or global search
476
497
  if (input.projectPath) {
477
498
  if (!result || !result.project || !result.project.issues) {
@@ -512,12 +533,17 @@ const searchMergeRequestsTool = {
512
533
  state: z.string().default('all').describe('Filter by merge request state (opened, closed, merged, all)'),
513
534
  first: z.number().min(1).max(100).default(20).describe('Number of merge requests to retrieve'),
514
535
  after: z.string().optional().describe('Cursor for pagination'),
536
+ sort: z.string().optional().describe('Sort order (e.g., UPDATED_DESC, CREATED_DESC, CREATED_ASC). Defaults to UPDATED_DESC for recency.'),
537
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
515
538
  })),
516
539
  handler: async (input, client, userConfig) => {
517
540
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
518
541
  // If projectPath provided, search in that project
519
542
  // Otherwise, intelligently find projects matching search term and search their MRs
520
- const result = await client.searchMergeRequests(input.searchTerm, input.projectPath, input.state, input.first, input.after, credentials);
543
+ const result = await client.searchMergeRequests(input.searchTerm, input.projectPath, input.state, input.first, input.after, input.fetchAll, credentials, input.sort);
544
+ if (input.fetchAll) {
545
+ return result;
546
+ }
521
547
  // Handle project-specific search
522
548
  if (input.projectPath) {
523
549
  if (!result || !result.project || !result.project.mergeRequests) {
@@ -553,10 +579,15 @@ const searchUsersTool = {
553
579
  .refine(val => val.length > 0, { message: 'Search term cannot be empty' })
554
580
  .describe('Search term to find users by username or name'),
555
581
  first: z.number().min(1).max(100).default(20).describe('Number of users to retrieve'),
582
+ after: z.string().optional().describe('Cursor for pagination'),
583
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
556
584
  })),
557
585
  handler: async (input, client, userConfig) => {
558
586
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
559
- const result = await client.searchUsers(input.searchTerm, input.first, credentials);
587
+ const result = await client.searchUsers(input.searchTerm, input.first, input.after, input.fetchAll, credentials);
588
+ if (input.fetchAll) {
589
+ return result;
590
+ }
560
591
  return result.users;
561
592
  },
562
593
  };
@@ -577,10 +608,15 @@ const searchGroupsTool = {
577
608
  .refine(val => val.length > 0, { message: 'Search term cannot be empty' })
578
609
  .describe('Search term to find groups by name or path'),
579
610
  first: z.number().min(1).max(100).default(20).describe('Number of groups to retrieve'),
611
+ after: z.string().optional().describe('Cursor for pagination'),
612
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
580
613
  })),
581
614
  handler: async (input, client, userConfig) => {
582
615
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
583
- const result = await client.searchGroups(input.searchTerm, input.first, credentials);
616
+ const result = await client.searchGroups(input.searchTerm, input.first, input.after, input.fetchAll, credentials);
617
+ if (input.fetchAll) {
618
+ return result;
619
+ }
584
620
  return result.groups;
585
621
  },
586
622
  };
@@ -660,6 +696,461 @@ const getFileContentTool = {
660
696
  };
661
697
  },
662
698
  };
699
+ // CI/CD Pipeline tools
700
+ const getMergeRequestPipelinesTool = {
701
+ name: 'get_merge_request_pipelines',
702
+ title: 'MR Pipelines',
703
+ description: 'Get CI/CD pipelines for a merge request, including status, duration, and stages',
704
+ requiresAuth: false,
705
+ requiresWrite: false,
706
+ annotations: {
707
+ readOnlyHint: true,
708
+ destructiveHint: false,
709
+ idempotentHint: true,
710
+ },
711
+ inputSchema: withUserAuth(z.object({
712
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
713
+ iid: z.string().describe('Merge request IID'),
714
+ first: z.number().min(1).max(100).default(20).describe('Number of pipelines to retrieve'),
715
+ after: z.string().optional().describe('Cursor for pagination'),
716
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
717
+ })),
718
+ handler: async (input, client, userConfig) => {
719
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
720
+ const result = await client.getMergeRequestPipelines(input.projectPath, input.iid, input.first, input.after, input.fetchAll, credentials);
721
+ if (input.fetchAll) {
722
+ return result;
723
+ }
724
+ if (!result?.project?.mergeRequest) {
725
+ throw new Error('Merge request not found');
726
+ }
727
+ return result.project.mergeRequest.pipelines;
728
+ },
729
+ };
730
+ const getPipelineJobsTool = {
731
+ name: 'get_pipeline_jobs',
732
+ title: 'Pipeline Jobs',
733
+ description: 'Get jobs for a specific pipeline, including status, stage, duration, and retry/cancel info',
734
+ requiresAuth: false,
735
+ requiresWrite: false,
736
+ annotations: {
737
+ readOnlyHint: true,
738
+ destructiveHint: false,
739
+ idempotentHint: true,
740
+ },
741
+ inputSchema: withUserAuth(z.object({
742
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
743
+ pipelineIid: z.string().describe('Pipeline IID'),
744
+ first: z.number().min(1).max(100).default(20).describe('Number of jobs to retrieve'),
745
+ after: z.string().optional().describe('Cursor for pagination'),
746
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
747
+ })),
748
+ handler: async (input, client, userConfig) => {
749
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
750
+ const result = await client.getPipelineJobs(input.projectPath, input.pipelineIid, input.first, input.after, input.fetchAll, credentials);
751
+ if (input.fetchAll) {
752
+ return result;
753
+ }
754
+ if (!result?.project?.pipeline) {
755
+ throw new Error('Pipeline not found');
756
+ }
757
+ return {
758
+ pipeline: {
759
+ id: result.project.pipeline.id,
760
+ iid: result.project.pipeline.iid,
761
+ status: result.project.pipeline.status,
762
+ },
763
+ jobs: result.project.pipeline.jobs,
764
+ };
765
+ },
766
+ };
767
+ const managePipelineTool = {
768
+ name: 'manage_pipeline',
769
+ title: 'Manage Pipeline',
770
+ description: 'Retry or cancel a CI/CD pipeline (requires user authentication with write permissions)',
771
+ requiresAuth: true,
772
+ requiresWrite: true,
773
+ annotations: {
774
+ readOnlyHint: false,
775
+ destructiveHint: false,
776
+ idempotentHint: false,
777
+ },
778
+ inputSchema: withUserAuth(z.object({
779
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
780
+ pipelineIid: z.string().describe('Pipeline IID'),
781
+ action: z.enum(['retry', 'cancel']).describe('Action to perform on the pipeline'),
782
+ })),
783
+ handler: async (input, client, userConfig) => {
784
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
785
+ if (!credentials) {
786
+ throw new Error('User authentication is required for pipeline management. Please provide your GitLab credentials.');
787
+ }
788
+ return await client.managePipeline(input.projectPath, input.pipelineIid, input.action, credentials);
789
+ },
790
+ };
791
+ // MR Diffs & Commits tools
792
+ const getMergeRequestDiffsTool = {
793
+ name: 'get_merge_request_diffs',
794
+ title: 'MR Diffs',
795
+ description: 'Get diff statistics for a merge request, including per-file additions/deletions and diff refs',
796
+ requiresAuth: false,
797
+ requiresWrite: false,
798
+ annotations: {
799
+ readOnlyHint: true,
800
+ destructiveHint: false,
801
+ idempotentHint: true,
802
+ },
803
+ inputSchema: withUserAuth(z.object({
804
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
805
+ iid: z.string().describe('Merge request IID'),
806
+ })),
807
+ handler: async (input, client, userConfig) => {
808
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
809
+ const result = await client.getMergeRequestDiffs(input.projectPath, input.iid, credentials);
810
+ if (!result?.project?.mergeRequest) {
811
+ throw new Error('Merge request not found');
812
+ }
813
+ return result.project.mergeRequest;
814
+ },
815
+ };
816
+ const getMergeRequestCommitsTool = {
817
+ name: 'get_merge_request_commits',
818
+ title: 'MR Commits',
819
+ description: 'Get commits for a merge request (excluding merge commits), with commit count and details',
820
+ requiresAuth: false,
821
+ requiresWrite: false,
822
+ annotations: {
823
+ readOnlyHint: true,
824
+ destructiveHint: false,
825
+ idempotentHint: true,
826
+ },
827
+ inputSchema: withUserAuth(z.object({
828
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
829
+ iid: z.string().describe('Merge request IID'),
830
+ first: z.number().min(1).max(100).default(20).describe('Number of commits to retrieve'),
831
+ after: z.string().optional().describe('Cursor for pagination'),
832
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
833
+ })),
834
+ handler: async (input, client, userConfig) => {
835
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
836
+ const result = await client.getMergeRequestCommits(input.projectPath, input.iid, input.first, input.after, input.fetchAll, credentials);
837
+ if (input.fetchAll) {
838
+ return result;
839
+ }
840
+ if (!result?.project?.mergeRequest) {
841
+ throw new Error('Merge request not found');
842
+ }
843
+ return {
844
+ commitCount: result.project.mergeRequest.commitCount,
845
+ commits: result.project.mergeRequest.commitsWithoutMergeCommits,
846
+ };
847
+ },
848
+ };
849
+ // Work Item Notes tools
850
+ const getNotesTool = {
851
+ name: 'get_notes',
852
+ title: 'Notes/Comments',
853
+ description: 'Get notes (comments) on an issue or merge request, including system notes and inline MR comments',
854
+ requiresAuth: false,
855
+ requiresWrite: false,
856
+ annotations: {
857
+ readOnlyHint: true,
858
+ destructiveHint: false,
859
+ idempotentHint: true,
860
+ },
861
+ inputSchema: withUserAuth(z.object({
862
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
863
+ noteableType: z.enum(['issue', 'merge_request']).describe('Type of item to get notes for'),
864
+ iid: z.string().describe('Issue or merge request IID'),
865
+ first: z.number().min(1).max(100).default(20).describe('Number of notes to retrieve'),
866
+ after: z.string().optional().describe('Cursor for pagination'),
867
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
868
+ })),
869
+ handler: async (input, client, userConfig) => {
870
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
871
+ const result = await client.getNotes(input.projectPath, input.noteableType, input.iid, input.first, input.after, input.fetchAll, credentials);
872
+ if (input.fetchAll) {
873
+ return result;
874
+ }
875
+ const noteable = input.noteableType === 'issue'
876
+ ? result?.project?.issue
877
+ : result?.project?.mergeRequest;
878
+ if (!noteable) {
879
+ throw new Error(`${input.noteableType === 'issue' ? 'Issue' : 'Merge request'} not found`);
880
+ }
881
+ return noteable.notes;
882
+ },
883
+ };
884
+ const createNoteTool = {
885
+ name: 'create_note',
886
+ title: 'Create Note',
887
+ description: 'Add a comment/note to an issue or merge request (requires user authentication)',
888
+ requiresAuth: true,
889
+ requiresWrite: true,
890
+ annotations: {
891
+ readOnlyHint: false,
892
+ destructiveHint: false,
893
+ idempotentHint: false,
894
+ },
895
+ inputSchema: withUserAuth(z.object({
896
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
897
+ noteableType: z.enum(['issue', 'merge_request']).describe('Type of item to add a note to'),
898
+ iid: z.string().describe('Issue or merge request IID'),
899
+ body: z.string().min(1).describe('Note body (supports Markdown)'),
900
+ internal: z.boolean().default(false).describe('Whether the note is internal/confidential (only visible to project members)'),
901
+ })),
902
+ handler: async (input, client, userConfig) => {
903
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
904
+ if (!credentials) {
905
+ throw new Error('User authentication is required for creating notes. Please provide your GitLab credentials.');
906
+ }
907
+ const result = await client.createNote(input.projectPath, input.noteableType, input.iid, input.body, input.internal, credentials);
908
+ if (result.errors && result.errors.length > 0) {
909
+ throw new Error(`Failed to create note: ${result.errors.join(', ')}`);
910
+ }
911
+ return result.note;
912
+ },
913
+ };
914
+ // ── Project Tracking & User Reporting tools ─────────────────────────
915
+ const listMilestonesTool = {
916
+ name: 'list_milestones',
917
+ title: 'Milestones',
918
+ description: 'List milestones for a project or group with progress statistics (total/closed issue counts)',
919
+ requiresAuth: false,
920
+ requiresWrite: false,
921
+ annotations: {
922
+ readOnlyHint: true,
923
+ destructiveHint: false,
924
+ idempotentHint: true,
925
+ },
926
+ inputSchema: withUserAuth(z.object({
927
+ fullPath: z.string().describe('Full path of the project or group (e.g., "group/project-name" or "group")'),
928
+ isProject: z.boolean().describe('Whether the path is a project (true) or group (false)'),
929
+ state: z.string().optional().describe('Filter by state: active, closed (omit for all)'),
930
+ search: z.string().optional().describe('Search milestones by title'),
931
+ includeAncestors: z.boolean().default(false).describe('Include milestones from ancestor groups'),
932
+ first: z.number().min(1).max(100).default(20).describe('Number of milestones to retrieve'),
933
+ after: z.string().optional().describe('Cursor for pagination'),
934
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
935
+ })),
936
+ handler: async (input, client, userConfig) => {
937
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
938
+ const result = await client.listMilestones(input.fullPath, input.isProject, input.state, input.search, input.includeAncestors, input.first, input.after, input.fetchAll, credentials);
939
+ if (input.fetchAll) {
940
+ return result;
941
+ }
942
+ const container = input.isProject ? result?.project : result?.group;
943
+ if (!container) {
944
+ throw new Error(`${input.isProject ? 'Project' : 'Group'} not found: ${input.fullPath}`);
945
+ }
946
+ return container.milestones;
947
+ },
948
+ };
949
+ const listIterationsTool = {
950
+ name: 'list_iterations',
951
+ title: 'Iterations',
952
+ description: 'List iterations (sprints) for a group with cadence info. Requires GitLab Premium/Ultimate.',
953
+ requiresAuth: false,
954
+ requiresWrite: false,
955
+ annotations: {
956
+ readOnlyHint: true,
957
+ destructiveHint: false,
958
+ idempotentHint: true,
959
+ },
960
+ inputSchema: withUserAuth(z.object({
961
+ groupPath: z.string().describe('Full path of the group (e.g., "my-group" or "parent/child-group")'),
962
+ state: z.string().optional().describe('Filter by state: upcoming, current, opened, closed (omit for all)'),
963
+ first: z.number().min(1).max(100).default(20).describe('Number of iterations to retrieve'),
964
+ after: z.string().optional().describe('Cursor for pagination'),
965
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
966
+ })),
967
+ handler: async (input, client, userConfig) => {
968
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
969
+ try {
970
+ const result = await client.listIterations(input.groupPath, input.state, input.first, input.after, input.fetchAll, credentials);
971
+ if (input.fetchAll) {
972
+ return result;
973
+ }
974
+ if (!result?.group) {
975
+ throw new Error(`Group not found: ${input.groupPath}`);
976
+ }
977
+ return result.group.iterations;
978
+ }
979
+ catch (error) {
980
+ if (error.message?.includes('iterations') || error.message?.includes('does not exist')) {
981
+ throw new Error(`Iterations are not available for "${input.groupPath}". ` +
982
+ `This feature requires GitLab Premium or Ultimate. ` +
983
+ `Original error: ${error.message}`);
984
+ }
985
+ throw error;
986
+ }
987
+ },
988
+ };
989
+ const getTimeTrackingTool = {
990
+ name: 'get_time_tracking',
991
+ title: 'Time Tracking',
992
+ description: 'Get time tracking data (estimate, spent, timelogs) for an issue or merge request',
993
+ requiresAuth: false,
994
+ requiresWrite: false,
995
+ annotations: {
996
+ readOnlyHint: true,
997
+ destructiveHint: false,
998
+ idempotentHint: true,
999
+ },
1000
+ inputSchema: withUserAuth(z.object({
1001
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
1002
+ resourceType: z.enum(['issue', 'merge_request']).describe('Type of resource to get time tracking for'),
1003
+ iid: z.string().describe('Issue or merge request IID'),
1004
+ includeTimelogs: z.boolean().default(true).describe('Whether to include individual timelog entries'),
1005
+ first: z.number().min(1).max(100).default(20).describe('Number of timelog entries to retrieve'),
1006
+ after: z.string().optional().describe('Cursor for pagination'),
1007
+ })),
1008
+ handler: async (input, client, userConfig) => {
1009
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1010
+ const result = await client.getTimeTracking(input.projectPath, input.resourceType, input.iid, input.includeTimelogs, input.first, input.after, credentials);
1011
+ const resource = input.resourceType === 'issue'
1012
+ ? result?.project?.issue
1013
+ : result?.project?.mergeRequest;
1014
+ if (!resource) {
1015
+ throw new Error(`${input.resourceType === 'issue' ? 'Issue' : 'Merge request'} not found`);
1016
+ }
1017
+ return resource;
1018
+ },
1019
+ };
1020
+ const getMergeRequestReviewersTool = {
1021
+ name: 'get_merge_request_reviewers',
1022
+ title: 'MR Reviewers',
1023
+ description: 'Get approval and reviewer status for a merge request, including who approved and review states',
1024
+ requiresAuth: false,
1025
+ requiresWrite: false,
1026
+ annotations: {
1027
+ readOnlyHint: true,
1028
+ destructiveHint: false,
1029
+ idempotentHint: true,
1030
+ },
1031
+ inputSchema: withUserAuth(z.object({
1032
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
1033
+ iid: z.string().describe('Merge request IID'),
1034
+ })),
1035
+ handler: async (input, client, userConfig) => {
1036
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1037
+ const result = await client.getMergeRequestReviewers(input.projectPath, input.iid, credentials);
1038
+ if (!result?.project?.mergeRequest) {
1039
+ throw new Error('Merge request not found');
1040
+ }
1041
+ return result.project.mergeRequest;
1042
+ },
1043
+ };
1044
+ const getProjectStatisticsTool = {
1045
+ name: 'get_project_statistics',
1046
+ title: 'Project Statistics',
1047
+ description: 'Get aggregate project statistics: open issues/MRs, star/fork counts, storage sizes, commit count, last pipeline status, release count, and language breakdown',
1048
+ requiresAuth: false,
1049
+ requiresWrite: false,
1050
+ annotations: {
1051
+ readOnlyHint: true,
1052
+ destructiveHint: false,
1053
+ idempotentHint: true,
1054
+ },
1055
+ inputSchema: withUserAuth(z.object({
1056
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
1057
+ })),
1058
+ handler: async (input, client, userConfig) => {
1059
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1060
+ const result = await client.getProjectStatistics(input.projectPath, credentials);
1061
+ if (!result?.project) {
1062
+ throw new Error(`Project not found: ${input.projectPath}`);
1063
+ }
1064
+ const p = result.project;
1065
+ return {
1066
+ id: p.id,
1067
+ name: p.name,
1068
+ fullPath: p.fullPath,
1069
+ webUrl: p.webUrl,
1070
+ starCount: p.starCount,
1071
+ forksCount: p.forksCount,
1072
+ openIssuesCount: p.openIssuesCount,
1073
+ openMergeRequestsCount: p.openMergeRequests?.count ?? null,
1074
+ commitCount: p.statistics?.commitCount ?? null,
1075
+ storage: p.statistics ? {
1076
+ repositorySize: p.statistics.repositorySize,
1077
+ lfsObjectsSize: p.statistics.lfsObjectsSize,
1078
+ buildArtifactsSize: p.statistics.buildArtifactsSize,
1079
+ packagesSize: p.statistics.packagesSize,
1080
+ wikiSize: p.statistics.wikiSize,
1081
+ snippetsSize: p.statistics.snippetsSize,
1082
+ uploadsSize: p.statistics.uploadsSize,
1083
+ containerRegistrySize: p.statistics.containerRegistrySize,
1084
+ } : null,
1085
+ lastPipeline: p.lastPipeline?.nodes?.[0] ?? null,
1086
+ releaseCount: p.releaseCount?.count ?? null,
1087
+ languages: p.languages ?? [],
1088
+ };
1089
+ },
1090
+ };
1091
+ const listGroupMembersTool = {
1092
+ name: 'list_group_members',
1093
+ title: 'Group Members',
1094
+ description: 'List group members with access levels, optionally filtered by search term',
1095
+ requiresAuth: false,
1096
+ requiresWrite: false,
1097
+ annotations: {
1098
+ readOnlyHint: true,
1099
+ destructiveHint: false,
1100
+ idempotentHint: true,
1101
+ },
1102
+ inputSchema: withUserAuth(z.object({
1103
+ groupPath: z.string().describe('Full path of the group (e.g., "my-group" or "parent/child-group")'),
1104
+ search: z.string().optional().describe('Optional search term to filter members by name or username'),
1105
+ first: z.number().min(1).max(100).default(20).describe('Number of members to retrieve'),
1106
+ after: z.string().optional().describe('Cursor for pagination'),
1107
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
1108
+ })),
1109
+ handler: async (input, client, userConfig) => {
1110
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1111
+ const result = await client.listGroupMembers(input.groupPath, input.search, input.first, input.after, input.fetchAll, credentials);
1112
+ if (input.fetchAll) {
1113
+ return result;
1114
+ }
1115
+ if (!result?.group) {
1116
+ throw new Error(`Group not found: ${input.groupPath}`);
1117
+ }
1118
+ return result.group.groupMembers;
1119
+ },
1120
+ };
1121
+ // Label Search tool
1122
+ const searchLabelsTool = {
1123
+ name: 'search_labels',
1124
+ title: 'Search Labels',
1125
+ description: 'Search for labels in a project or group, with optional text filtering',
1126
+ requiresAuth: false,
1127
+ requiresWrite: false,
1128
+ annotations: {
1129
+ readOnlyHint: true,
1130
+ destructiveHint: false,
1131
+ idempotentHint: true,
1132
+ },
1133
+ inputSchema: withUserAuth(z.object({
1134
+ fullPath: z.string().describe('Full path of the project or group (e.g., "group/project-name" or "group")'),
1135
+ isProject: z.boolean().describe('Whether the path is a project (true) or group (false)'),
1136
+ search: z.string().optional().describe('Optional search term to filter labels'),
1137
+ first: z.number().min(1).max(100).default(20).describe('Number of labels to retrieve'),
1138
+ after: z.string().optional().describe('Cursor for pagination'),
1139
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
1140
+ })),
1141
+ handler: async (input, client, userConfig) => {
1142
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1143
+ const result = await client.searchLabels(input.fullPath, input.isProject, input.search, input.first, input.after, input.fetchAll, credentials);
1144
+ if (input.fetchAll) {
1145
+ return result;
1146
+ }
1147
+ const container = input.isProject ? result?.project : result?.group;
1148
+ if (!container) {
1149
+ throw new Error(`${input.isProject ? 'Project' : 'Group'} not found: ${input.fullPath}`);
1150
+ }
1151
+ return container.labels;
1152
+ },
1153
+ };
663
1154
  // Helper functions for common user queries
664
1155
  const getUserIssuesTool = {
665
1156
  name: 'get_user_issues',
@@ -676,17 +1167,21 @@ const getUserIssuesTool = {
676
1167
  username: z.string().describe('Username to find issues for (e.g., "cdhanlon")'),
677
1168
  state: z.string().default('opened').describe('Filter by issue state (opened, closed, all)'),
678
1169
  projectPath: z.string().optional().describe('Optional: limit search to a specific project'),
679
- first: z.number().min(1).max(100).default(50).describe('Number of issues to retrieve'),
1170
+ first: z.number().min(1).max(100).default(20).describe('Number of issues to retrieve'),
680
1171
  after: z.string().optional().describe('Cursor for pagination'),
1172
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
681
1173
  })),
682
1174
  handler: async (input, client, userConfig) => {
683
1175
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
684
1176
  // Use the searchIssues method with assigneeUsernames filter
685
1177
  const result = await client.searchIssues(undefined, // No text search
686
- input.projectPath, input.state, input.first, input.after, credentials, [input.username], // assigneeUsernames
1178
+ input.projectPath, input.state, input.first, input.after, input.fetchAll, credentials, [input.username], // assigneeUsernames
687
1179
  undefined, // authorUsername
688
1180
  undefined // labelNames
689
1181
  );
1182
+ if (input.fetchAll) {
1183
+ return result;
1184
+ }
690
1185
  if (input.projectPath) {
691
1186
  if (!result || !result.project || !result.project.issues) {
692
1187
  throw new Error('Project not found or issues are not accessible for the provided path');
@@ -723,14 +1218,18 @@ const getUserMergeRequestsTool = {
723
1218
  role: z.enum(['author', 'assignee']).default('author').describe('Whether to find MRs authored by or assigned to the user'),
724
1219
  state: z.string().default('opened').describe('Filter by MR state (opened, closed, merged, all)'),
725
1220
  projectPath: z.string().optional().describe('Optional: limit search to a specific project'),
726
- first: z.number().min(1).max(100).default(50).describe('Number of merge requests to retrieve'),
1221
+ first: z.number().min(1).max(100).default(20).describe('Number of merge requests to retrieve'),
727
1222
  after: z.string().optional().describe('Cursor for pagination'),
1223
+ fetchAll: z.boolean().default(false).describe('Fetch all pages up to 100 results'),
728
1224
  })),
729
1225
  handler: async (input, client, userConfig) => {
730
1226
  const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
731
1227
  // Use the searchMergeRequests method with author: or assignee: prefix
732
1228
  const searchTerm = input.role === 'author' ? `author:${input.username}` : `assignee:${input.username}`;
733
- const result = await client.searchMergeRequests(searchTerm, input.projectPath, input.state, input.first, input.after, credentials);
1229
+ const result = await client.searchMergeRequests(searchTerm, input.projectPath, input.state, input.first, input.after, input.fetchAll, credentials);
1230
+ if (input.fetchAll) {
1231
+ return result;
1232
+ }
734
1233
  if (input.projectPath) {
735
1234
  if (!result || !result.project || !result.project.mergeRequests) {
736
1235
  throw new Error(`Project "${input.projectPath}" not found or merge requests are not accessible`);
@@ -751,6 +1250,146 @@ const getUserMergeRequestsTool = {
751
1250
  };
752
1251
  },
753
1252
  };
1253
+ const BroadcastMessageFields = {
1254
+ message: z.string().min(1).describe('Message text to display'),
1255
+ starts_at: z.string().datetime().optional().describe('ISO 8601 timestamp when the message starts'),
1256
+ ends_at: z.string().datetime().optional().describe('ISO 8601 timestamp when the message ends'),
1257
+ color: z.string().optional().describe('Background color in hex format, e.g. "#E75E40"'),
1258
+ font: z.string().optional().describe('Foreground (font) color in hex format'),
1259
+ target_access_levels: z.array(z.number().int()).optional().describe('Access levels to target: 10=Guest, 20=Reporter, 30=Developer, 40=Maintainer, 50=Owner'),
1260
+ target_path: z.string().optional().describe('Path glob for pages where the message should appear'),
1261
+ broadcast_type: z.enum(['banner', 'notification']).optional().describe('Broadcast type: "banner" or "notification"'),
1262
+ dismissable: z.boolean().optional().describe('Whether users can dismiss the broadcast message'),
1263
+ theme: z.string().optional().describe('Theme name (GitLab 16.9+), e.g. "indigo", "red"'),
1264
+ };
1265
+ const listBroadcastMessagesTool = {
1266
+ name: 'list_broadcast_messages',
1267
+ title: 'List Broadcast Messages',
1268
+ description: 'List all GitLab broadcast messages (instance-wide announcements). Read-only.',
1269
+ requiresAuth: false,
1270
+ requiresWrite: false,
1271
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
1272
+ inputSchema: withUserAuth(z.object({
1273
+ page: z.number().int().min(1).default(1).describe('Page number (1-based)'),
1274
+ perPage: z.number().int().min(1).max(100).default(20).describe('Results per page'),
1275
+ })),
1276
+ handler: async (input, client, userConfig) => {
1277
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1278
+ return client.listBroadcastMessages(input.page, input.perPage, credentials);
1279
+ },
1280
+ };
1281
+ const getBroadcastMessageTool = {
1282
+ name: 'get_broadcast_message',
1283
+ title: 'Get Broadcast Message',
1284
+ description: 'Get a specific GitLab broadcast message by ID.',
1285
+ requiresAuth: false,
1286
+ requiresWrite: false,
1287
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
1288
+ inputSchema: withUserAuth(z.object({
1289
+ id: z.number().int().describe('Broadcast message ID'),
1290
+ })),
1291
+ handler: async (input, client, userConfig) => {
1292
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1293
+ return client.getBroadcastMessage(input.id, credentials);
1294
+ },
1295
+ };
1296
+ const createBroadcastMessageTool = {
1297
+ name: 'create_broadcast_message',
1298
+ title: 'Create Broadcast Message',
1299
+ description: 'Create a GitLab broadcast message. Requires administrator privileges on the GitLab instance.',
1300
+ requiresAuth: true,
1301
+ requiresWrite: true,
1302
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
1303
+ inputSchema: withUserAuth(z.object(BroadcastMessageFields)),
1304
+ handler: async (input, client, userConfig) => {
1305
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1306
+ if (!credentials) {
1307
+ throw new Error('User authentication is required for creating broadcast messages.');
1308
+ }
1309
+ const { userCredentials, ...body } = input;
1310
+ return client.createBroadcastMessage(body, credentials);
1311
+ },
1312
+ };
1313
+ const updateBroadcastMessageTool = {
1314
+ name: 'update_broadcast_message',
1315
+ title: 'Update Broadcast Message',
1316
+ description: 'Update an existing GitLab broadcast message. Requires administrator privileges.',
1317
+ requiresAuth: true,
1318
+ requiresWrite: true,
1319
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
1320
+ inputSchema: withUserAuth(z.object({
1321
+ id: z.number().int().describe('Broadcast message ID'),
1322
+ message: BroadcastMessageFields.message.optional(),
1323
+ starts_at: BroadcastMessageFields.starts_at,
1324
+ ends_at: BroadcastMessageFields.ends_at,
1325
+ color: BroadcastMessageFields.color,
1326
+ font: BroadcastMessageFields.font,
1327
+ target_access_levels: BroadcastMessageFields.target_access_levels,
1328
+ target_path: BroadcastMessageFields.target_path,
1329
+ broadcast_type: BroadcastMessageFields.broadcast_type,
1330
+ dismissable: BroadcastMessageFields.dismissable,
1331
+ theme: BroadcastMessageFields.theme,
1332
+ })),
1333
+ handler: async (input, client, userConfig) => {
1334
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1335
+ if (!credentials) {
1336
+ throw new Error('User authentication is required for updating broadcast messages.');
1337
+ }
1338
+ const { id, userCredentials, ...body } = input;
1339
+ return client.updateBroadcastMessage(id, body, credentials);
1340
+ },
1341
+ };
1342
+ const deleteBroadcastMessageTool = {
1343
+ name: 'delete_broadcast_message',
1344
+ title: 'Delete Broadcast Message',
1345
+ description: 'Delete a GitLab broadcast message by ID. Requires administrator privileges.',
1346
+ requiresAuth: true,
1347
+ requiresWrite: true,
1348
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
1349
+ inputSchema: withUserAuth(z.object({
1350
+ id: z.number().int().describe('Broadcast message ID'),
1351
+ })),
1352
+ handler: async (input, client, userConfig) => {
1353
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
1354
+ if (!credentials) {
1355
+ throw new Error('User authentication is required for deleting broadcast messages.');
1356
+ }
1357
+ await client.deleteBroadcastMessage(input.id, credentials);
1358
+ return { id: input.id, deleted: true };
1359
+ },
1360
+ };
1361
+ export const readOnlyTools = [
1362
+ getProjectTool,
1363
+ getIssuesTool,
1364
+ getMergeRequestsTool,
1365
+ executeCustomQueryTool,
1366
+ getAvailableQueriesTools,
1367
+ getMergeRequestPipelinesTool,
1368
+ getPipelineJobsTool,
1369
+ getMergeRequestDiffsTool,
1370
+ getMergeRequestCommitsTool,
1371
+ getNotesTool,
1372
+ listMilestonesTool,
1373
+ listIterationsTool,
1374
+ getTimeTrackingTool,
1375
+ getMergeRequestReviewersTool,
1376
+ getProjectStatisticsTool,
1377
+ listBroadcastMessagesTool,
1378
+ getBroadcastMessageTool,
1379
+ ];
1380
+ export const userAuthTools = [
1381
+ getCurrentUserTool,
1382
+ getProjectsTool,
1383
+ ];
1384
+ export const writeTools = [
1385
+ createIssueTool,
1386
+ createMergeRequestTool,
1387
+ createNoteTool,
1388
+ managePipelineTool,
1389
+ createBroadcastMessageTool,
1390
+ updateBroadcastMessageTool,
1391
+ deleteBroadcastMessageTool,
1392
+ ];
754
1393
  export const searchTools = [
755
1394
  globalSearchTool,
756
1395
  searchProjectsTool,
@@ -760,8 +1399,10 @@ export const searchTools = [
760
1399
  getUserMergeRequestsTool,
761
1400
  searchUsersTool,
762
1401
  searchGroupsTool,
1402
+ searchLabelsTool,
763
1403
  browseRepositoryTool,
764
1404
  getFileContentTool,
1405
+ listGroupMembersTool,
765
1406
  ];
766
1407
  export const tools = [
767
1408
  ...readOnlyTools,