@ttpears/gitlab-mcp-server 1.7.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 ADDED
@@ -0,0 +1,777 @@
1
+ import { z } from 'zod';
2
+ import { validateUserConfig } from './config.js';
3
+ // Schema for user credentials (empty object coerces to undefined for header-based auth)
4
+ const UserCredentialsSchema = z.object({
5
+ gitlabUrl: z.string().url().optional(),
6
+ accessToken: z.string().min(1).optional(),
7
+ })
8
+ .nullable()
9
+ .optional()
10
+ .transform((val) => {
11
+ if (!val || !val.accessToken)
12
+ return undefined;
13
+ return val;
14
+ });
15
+ // Helper to add user credentials to input schemas
16
+ const withUserAuth = (baseSchema, required = false) => {
17
+ if (required) {
18
+ return baseSchema.extend({
19
+ userCredentials: z.object({
20
+ gitlabUrl: z.string().url().optional(),
21
+ accessToken: z.string().min(1),
22
+ }).nullable().describe('Your GitLab credentials (required for this operation)'),
23
+ });
24
+ }
25
+ else {
26
+ return baseSchema.extend({
27
+ userCredentials: UserCredentialsSchema.describe('Your GitLab credentials (optional - uses shared token if not provided)'),
28
+ });
29
+ }
30
+ };
31
+ // Read-only tools (can use shared token)
32
+ const getCurrentUserTool = {
33
+ name: 'get_current_user',
34
+ title: 'Current User',
35
+ description: 'Get information about the current authenticated GitLab user',
36
+ requiresAuth: true,
37
+ requiresWrite: false,
38
+ annotations: {
39
+ readOnlyHint: true,
40
+ destructiveHint: false,
41
+ idempotentHint: true,
42
+ },
43
+ inputSchema: withUserAuth(z.object({}).strict()),
44
+ handler: async (input, client, userConfig) => {
45
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
46
+ const result = await client.getCurrentUser(credentials);
47
+ return result.currentUser;
48
+ },
49
+ };
50
+ const getProjectTool = {
51
+ name: 'get_project',
52
+ title: 'Project Details',
53
+ description: 'Get detailed information about a specific GitLab project (read-only)',
54
+ requiresAuth: false,
55
+ requiresWrite: false,
56
+ annotations: {
57
+ readOnlyHint: true,
58
+ destructiveHint: false,
59
+ idempotentHint: true,
60
+ },
61
+ inputSchema: withUserAuth(z.object({
62
+ fullPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
63
+ })),
64
+ handler: async (input, client, userConfig) => {
65
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
66
+ const result = await client.getProject(input.fullPath, credentials);
67
+ return result.project;
68
+ },
69
+ };
70
+ const getProjectsTool = {
71
+ name: 'get_projects',
72
+ title: 'List Projects',
73
+ description: 'List projects accessible to the user (requires authentication to see private projects)',
74
+ requiresAuth: true,
75
+ requiresWrite: false,
76
+ annotations: {
77
+ readOnlyHint: true,
78
+ destructiveHint: false,
79
+ idempotentHint: true,
80
+ },
81
+ inputSchema: withUserAuth(z.object({
82
+ first: z.number().min(1).max(100).default(20).describe('Number of projects to retrieve'),
83
+ after: z.string().optional().describe('Cursor for pagination'),
84
+ })),
85
+ handler: async (input, client, userConfig) => {
86
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
87
+ const result = await client.getProjects(input.first, input.after, credentials);
88
+ return result.projects;
89
+ },
90
+ };
91
+ const getIssuesTool = {
92
+ name: 'get_issues',
93
+ title: 'Project Issues',
94
+ description: 'Get issues from a specific GitLab project (read-only)',
95
+ requiresAuth: false,
96
+ requiresWrite: false,
97
+ annotations: {
98
+ readOnlyHint: true,
99
+ destructiveHint: false,
100
+ idempotentHint: true,
101
+ },
102
+ inputSchema: withUserAuth(z.object({
103
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
104
+ first: z.number().min(1).max(100).default(20).describe('Number of issues to retrieve'),
105
+ after: z.string().optional().describe('Cursor for pagination'),
106
+ })),
107
+ handler: async (input, client, userConfig) => {
108
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
109
+ const result = await client.getIssues(input.projectPath, input.first, input.after, credentials);
110
+ if (!result || !result.project || !result.project.issues) {
111
+ throw new Error('Project not found or issues are not accessible for the provided path');
112
+ }
113
+ return result.project.issues;
114
+ },
115
+ };
116
+ const getMergeRequestsTool = {
117
+ name: 'get_merge_requests',
118
+ title: 'Merge Requests',
119
+ description: 'Get merge requests from a specific GitLab project (read-only)',
120
+ requiresAuth: false,
121
+ requiresWrite: false,
122
+ annotations: {
123
+ readOnlyHint: true,
124
+ destructiveHint: false,
125
+ idempotentHint: true,
126
+ },
127
+ inputSchema: withUserAuth(z.object({
128
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
129
+ first: z.number().min(1).max(100).default(20).describe('Number of merge requests to retrieve'),
130
+ after: z.string().optional().describe('Cursor for pagination'),
131
+ })),
132
+ handler: async (input, client, userConfig) => {
133
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
134
+ const result = await client.getMergeRequests(input.projectPath, input.first, input.after, credentials);
135
+ if (!result || !result.project || !result.project.mergeRequests) {
136
+ throw new Error('Project not found or merge requests are not accessible for the provided path');
137
+ }
138
+ return result.project.mergeRequests;
139
+ },
140
+ };
141
+ // Write operations (require user authentication)
142
+ const createIssueTool = {
143
+ name: 'create_issue',
144
+ title: 'Create Issue',
145
+ description: 'Create a new issue in a GitLab project (requires user authentication with write permissions)',
146
+ requiresAuth: true,
147
+ requiresWrite: true,
148
+ annotations: {
149
+ readOnlyHint: false,
150
+ destructiveHint: false,
151
+ idempotentHint: false,
152
+ },
153
+ inputSchema: withUserAuth(z.object({
154
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
155
+ title: z.string().min(1).describe('Title of the issue'),
156
+ description: z.string().optional().describe('Description of the issue'),
157
+ })),
158
+ handler: async (input, client, userConfig) => {
159
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
160
+ if (!credentials) {
161
+ throw new Error('User authentication is required for creating issues. Please provide your GitLab credentials.');
162
+ }
163
+ const result = await client.createIssue(input.projectPath, input.title, input.description, credentials);
164
+ const payload = result.createIssue;
165
+ if (payload.errors && payload.errors.length > 0) {
166
+ throw new Error(`Failed to create issue: ${payload.errors.join(', ')}`);
167
+ }
168
+ return payload.issue;
169
+ },
170
+ };
171
+ const createMergeRequestTool = {
172
+ name: 'create_merge_request',
173
+ title: 'Create Merge Request',
174
+ description: 'Create a new merge request in a GitLab project (requires user authentication with write permissions)',
175
+ requiresAuth: true,
176
+ requiresWrite: true,
177
+ annotations: {
178
+ readOnlyHint: false,
179
+ destructiveHint: false,
180
+ idempotentHint: false,
181
+ },
182
+ inputSchema: withUserAuth(z.object({
183
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
184
+ title: z.string().min(1).describe('Title of the merge request'),
185
+ sourceBranch: z.string().min(1).describe('Source branch name'),
186
+ targetBranch: z.string().min(1).describe('Target branch name'),
187
+ description: z.string().optional().describe('Description of the merge request'),
188
+ })),
189
+ handler: async (input, client, userConfig) => {
190
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
191
+ if (!credentials) {
192
+ throw new Error('User authentication is required for creating merge requests. Please provide your GitLab credentials.');
193
+ }
194
+ const result = await client.createMergeRequest(input.projectPath, input.title, input.sourceBranch, input.targetBranch, input.description, credentials);
195
+ const payload = result.createMergeRequest;
196
+ if (payload.errors && payload.errors.length > 0) {
197
+ throw new Error(`Failed to create merge request: ${payload.errors.join(', ')}`);
198
+ }
199
+ return payload.mergeRequest;
200
+ },
201
+ };
202
+ // Advanced tools
203
+ const executeCustomQueryTool = {
204
+ name: 'execute_custom_query',
205
+ title: 'Custom GraphQL Query',
206
+ description: 'Execute custom GraphQL queries for complex filtering (e.g., issues with assigneeUsernames: ["user"], labelName: ["bug"]). Use this for structured filtering by assignee/author/labels when search tools return 0 results. Use pagination and limit complexity to avoid timeouts.',
207
+ requiresAuth: false,
208
+ requiresWrite: false,
209
+ annotations: {
210
+ readOnlyHint: false,
211
+ destructiveHint: false,
212
+ idempotentHint: false,
213
+ },
214
+ inputSchema: withUserAuth(z.object({
215
+ query: z.string().describe('GraphQL query string. Example: query { issues(assigneeUsernames: ["cdhanlon"], state: opened, first: 50) { nodes { iid title webUrl } } }'),
216
+ variables: z.record(z.any()).optional().describe('Variables for the GraphQL query'),
217
+ requiresWrite: z.boolean().default(false).describe('Set to true if this is a mutation that requires write permissions'),
218
+ })),
219
+ handler: async (input, client, userConfig) => {
220
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
221
+ if (input.requiresWrite && !credentials) {
222
+ throw new Error('User authentication is required for write operations. Please provide your GitLab credentials.');
223
+ }
224
+ return await client.query(input.query, input.variables, credentials, input.requiresWrite);
225
+ },
226
+ };
227
+ const getAvailableQueriesTools = {
228
+ name: 'get_available_queries',
229
+ title: 'Available Queries',
230
+ description: 'Get list of available GraphQL queries and mutations from the GitLab schema',
231
+ requiresAuth: false,
232
+ requiresWrite: false,
233
+ annotations: {
234
+ readOnlyHint: true,
235
+ destructiveHint: false,
236
+ idempotentHint: true,
237
+ },
238
+ inputSchema: withUserAuth(z.object({}).strict()),
239
+ handler: async (input, client, userConfig) => {
240
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
241
+ await client.introspectSchema(credentials);
242
+ return {
243
+ queries: client.getAvailableQueries(),
244
+ mutations: client.getAvailableMutations(),
245
+ };
246
+ },
247
+ };
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
+ const updateIssueTool = {
264
+ name: 'update_issue',
265
+ title: 'Update Issue',
266
+ description: 'Update an issue (title, description, assignees, labels, due date) with schema-aware mutations',
267
+ requiresAuth: true,
268
+ requiresWrite: true,
269
+ annotations: {
270
+ readOnlyHint: false,
271
+ destructiveHint: false,
272
+ idempotentHint: true,
273
+ },
274
+ inputSchema: withUserAuth(z.object({
275
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
276
+ iid: z.string().describe('Issue IID (internal ID shown in the URL)'),
277
+ title: z.string().optional(),
278
+ description: z.string().optional(),
279
+ assigneeUsernames: z.array(z.string()).optional(),
280
+ labelNames: z.array(z.string()).optional(),
281
+ dueDate: z.string().optional().describe('YYYY-MM-DD'),
282
+ })),
283
+ handler: async (input, client, userConfig) => {
284
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
285
+ if (!credentials) {
286
+ throw new Error('User authentication is required to update issues.');
287
+ }
288
+ const result = await client.updateIssueComposite(input.projectPath, input.iid, {
289
+ title: input.title,
290
+ description: input.description,
291
+ assigneeUsernames: input.assigneeUsernames,
292
+ labelNames: input.labelNames,
293
+ dueDate: input.dueDate,
294
+ }, credentials);
295
+ return result;
296
+ },
297
+ };
298
+ const updateMergeRequestTool = {
299
+ name: 'update_merge_request',
300
+ title: 'Update Merge Request',
301
+ description: 'Update a merge request (title, description, assignees, reviewers, labels) with schema-aware mutations',
302
+ requiresAuth: true,
303
+ requiresWrite: true,
304
+ annotations: {
305
+ readOnlyHint: false,
306
+ destructiveHint: false,
307
+ idempotentHint: true,
308
+ },
309
+ inputSchema: withUserAuth(z.object({
310
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
311
+ iid: z.string().describe('Merge Request IID (internal ID shown in the URL)'),
312
+ title: z.string().optional(),
313
+ description: z.string().optional(),
314
+ assigneeUsernames: z.array(z.string()).optional(),
315
+ reviewerUsernames: z.array(z.string()).optional(),
316
+ labelNames: z.array(z.string()).optional(),
317
+ })),
318
+ handler: async (input, client, userConfig) => {
319
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
320
+ if (!credentials) {
321
+ throw new Error('User authentication is required to update merge requests.');
322
+ }
323
+ const result = await client.updateMergeRequestComposite(input.projectPath, input.iid, {
324
+ title: input.title,
325
+ description: input.description,
326
+ assigneeUsernames: input.assigneeUsernames,
327
+ reviewerUsernames: input.reviewerUsernames,
328
+ labelNames: input.labelNames,
329
+ }, credentials);
330
+ return result;
331
+ },
332
+ };
333
+ // Discovery/introspection tools
334
+ const resolvePathTool = {
335
+ name: 'resolve_path',
336
+ title: 'Resolve Path',
337
+ description: 'Resolve a GitLab path to either a project or group and list group projects when applicable',
338
+ requiresAuth: false,
339
+ requiresWrite: false,
340
+ annotations: {
341
+ readOnlyHint: true,
342
+ destructiveHint: false,
343
+ idempotentHint: true,
344
+ },
345
+ inputSchema: withUserAuth(z.object({
346
+ fullPath: z.string().min(1).describe('Project or group full path (e.g., "group/subgroup/project")'),
347
+ first: z.number().min(1).max(100).default(20).describe('Number of items to retrieve when listing group projects'),
348
+ after: z.string().optional().describe('Cursor for pagination'),
349
+ })),
350
+ handler: async (input, client, userConfig) => {
351
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
352
+ const result = await client.resolvePath(input.fullPath, input.first, input.after, credentials);
353
+ return result;
354
+ },
355
+ };
356
+ const getGroupProjectsTool = {
357
+ name: 'get_group_projects',
358
+ title: 'Group Projects',
359
+ description: 'List projects inside a GitLab group (optionally filter by search term)',
360
+ requiresAuth: false,
361
+ requiresWrite: false,
362
+ annotations: {
363
+ readOnlyHint: true,
364
+ destructiveHint: false,
365
+ idempotentHint: true,
366
+ },
367
+ inputSchema: withUserAuth(z.object({
368
+ fullPath: z.string().min(1).describe('Group full path (e.g., "group/subgroup")'),
369
+ searchTerm: z.string().optional().transform(v => v?.trim() || undefined).describe('Optional search term to filter group projects'),
370
+ first: z.number().min(1).max(100).default(20).describe('Number of projects to retrieve'),
371
+ after: z.string().optional().describe('Cursor for pagination'),
372
+ })),
373
+ handler: async (input, client, userConfig) => {
374
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
375
+ const result = await client.getGroup(input.fullPath, input.first, input.after, input.searchTerm, credentials);
376
+ return result.group;
377
+ },
378
+ };
379
+ const getTypeFieldsTool = {
380
+ name: 'get_type_fields',
381
+ title: 'GraphQL Type Fields',
382
+ description: 'List available fields on a GraphQL type using introspected schema (requires schema to be introspected)',
383
+ requiresAuth: false,
384
+ requiresWrite: false,
385
+ annotations: {
386
+ readOnlyHint: true,
387
+ destructiveHint: false,
388
+ idempotentHint: true,
389
+ },
390
+ inputSchema: withUserAuth(z.object({
391
+ typeName: z.string().min(1).describe('GraphQL type name (e.g., "Project")'),
392
+ })),
393
+ handler: async (input, client, userConfig) => {
394
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
395
+ await client.introspectSchema(credentials);
396
+ return { typeName: input.typeName, fields: client.getTypeFields(input.typeName) };
397
+ },
398
+ };
399
+ // Search tools - comprehensive search capabilities for LLMs
400
+ const globalSearchTool = {
401
+ name: 'search_gitlab',
402
+ title: 'Search GitLab',
403
+ description: 'Text search across GitLab projects and issues (Note: Does not support filtering by assignee/labels - use search_issues for that. MRs cannot be searched globally - use search_merge_requests with username)',
404
+ requiresAuth: false,
405
+ requiresWrite: false,
406
+ annotations: {
407
+ readOnlyHint: true,
408
+ destructiveHint: false,
409
+ idempotentHint: true,
410
+ },
411
+ inputSchema: withUserAuth(z.object({
412
+ searchTerm: z.string().optional().transform(val => val?.trim() || undefined).describe('Search term (leave empty for recent activity)'),
413
+ })),
414
+ handler: async (input, client, userConfig) => {
415
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
416
+ const result = await client.globalSearch(input.searchTerm, undefined, credentials);
417
+ return {
418
+ searchTerm: input.searchTerm,
419
+ projects: result.projects.nodes,
420
+ issues: result.issues.nodes,
421
+ totalResults: result.projects.nodes.length + result.issues.nodes.length,
422
+ _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
+ };
424
+ },
425
+ };
426
+ const searchProjectsTool = {
427
+ name: 'search_projects',
428
+ title: 'Search Projects',
429
+ description: 'Search for GitLab projects by name or description',
430
+ requiresAuth: false,
431
+ requiresWrite: false,
432
+ annotations: {
433
+ readOnlyHint: true,
434
+ destructiveHint: false,
435
+ idempotentHint: true,
436
+ },
437
+ inputSchema: withUserAuth(z.object({
438
+ searchTerm: z.string()
439
+ .transform(val => val.trim())
440
+ .refine(val => val.length > 0, { message: 'Search term cannot be empty' })
441
+ .describe('Search term to find projects by name or description'),
442
+ first: z.number().min(1).max(100).default(20).describe('Number of projects to retrieve'),
443
+ after: z.string().optional().describe('Cursor for pagination'),
444
+ })),
445
+ handler: async (input, client, userConfig) => {
446
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
447
+ const result = await client.searchProjects(input.searchTerm, input.first, input.after, credentials);
448
+ return result.projects;
449
+ },
450
+ };
451
+ const searchIssuesTool = {
452
+ name: 'search_issues',
453
+ title: 'Search Issues',
454
+ description: 'Search for issues with text search and/or structured filtering (assignee, author, labels, state). For filtering by assignee/author/labels without text search, leave searchTerm empty.',
455
+ requiresAuth: false,
456
+ requiresWrite: false,
457
+ annotations: {
458
+ readOnlyHint: true,
459
+ destructiveHint: false,
460
+ idempotentHint: true,
461
+ },
462
+ inputSchema: withUserAuth(z.object({
463
+ searchTerm: z.string().optional().transform(val => val?.trim() || undefined).describe('Text search term (optional - leave empty to filter by assignee/author/labels only)'),
464
+ projectPath: z.string().optional().describe('Optional project path (e.g., "group/project"). Omit for global search.'),
465
+ state: z.string().default('all').describe('Filter by issue state (opened, closed, all)'),
466
+ assigneeUsernames: z.array(z.string()).optional().describe('Filter by assignee usernames (e.g., ["cdhanlon", "jsmith"])'),
467
+ authorUsername: z.string().optional().describe('Filter by author username (e.g., "cdhanlon")'),
468
+ labelNames: z.array(z.string()).optional().describe('Filter by label names (e.g., ["Priority::High", "bug"])'),
469
+ first: z.number().min(1).max(100).default(20).describe('Number of issues to retrieve'),
470
+ after: z.string().optional().describe('Cursor for pagination'),
471
+ })),
472
+ handler: async (input, client, userConfig) => {
473
+ 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);
475
+ // Return the issues from either project-specific or global search
476
+ if (input.projectPath) {
477
+ if (!result || !result.project || !result.project.issues) {
478
+ throw new Error('Project not found or issues are not accessible for the provided path');
479
+ }
480
+ return result.project.issues;
481
+ }
482
+ else {
483
+ const issues = result.issues;
484
+ // If no results and filters were used, provide helpful error message
485
+ if (issues.nodes.length === 0 && (input.assigneeUsernames || input.authorUsername || input.labelNames)) {
486
+ return {
487
+ ...issues,
488
+ _note: 'No issues found with the specified filters. Try: (1) checking username/label spelling, (2) broadening filters, or (3) using execute_custom_query for complex filtering.'
489
+ };
490
+ }
491
+ return issues;
492
+ }
493
+ },
494
+ };
495
+ const searchMergeRequestsTool = {
496
+ name: 'search_merge_requests',
497
+ title: 'Search Merge Requests',
498
+ description: 'Search merge requests by username (supports "username", "author:username", "assignee:username") or search within a specific project. Note: GitLab does not support global text search for MRs - use projectPath for text searches.',
499
+ requiresAuth: false,
500
+ requiresWrite: false,
501
+ annotations: {
502
+ readOnlyHint: true,
503
+ destructiveHint: false,
504
+ idempotentHint: true,
505
+ },
506
+ inputSchema: withUserAuth(z.object({
507
+ searchTerm: z.string()
508
+ .transform(val => val.trim())
509
+ .refine(val => val.length > 0, { message: 'Search term cannot be empty' })
510
+ .describe('Username (e.g., "cdhanlon", "author:username", "assignee:username") or text when projectPath provided'),
511
+ projectPath: z.string().optional().describe('Project path (e.g., "group/project"). Required for text searches, optional for username searches.'),
512
+ state: z.string().default('all').describe('Filter by merge request state (opened, closed, merged, all)'),
513
+ first: z.number().min(1).max(100).default(20).describe('Number of merge requests to retrieve'),
514
+ after: z.string().optional().describe('Cursor for pagination'),
515
+ })),
516
+ handler: async (input, client, userConfig) => {
517
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
518
+ // If projectPath provided, search in that project
519
+ // 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);
521
+ // Handle project-specific search
522
+ if (input.projectPath) {
523
+ if (!result || !result.project || !result.project.mergeRequests) {
524
+ throw new Error(`Project "${input.projectPath}" not found or merge requests are not accessible`);
525
+ }
526
+ return result.project.mergeRequests;
527
+ }
528
+ // Handle intelligent global search (username-based)
529
+ // Provide helpful note if no results found
530
+ if (result.nodes && result.nodes.length === 0) {
531
+ return {
532
+ ...result,
533
+ _note: 'No merge requests found. For username searches, ensure the username is correct. For text searches, provide a projectPath.'
534
+ };
535
+ }
536
+ return result;
537
+ },
538
+ };
539
+ const searchUsersTool = {
540
+ name: 'search_users',
541
+ title: 'Search Users',
542
+ description: 'Search for GitLab users by username or name - useful for finding team members or contributors',
543
+ requiresAuth: false,
544
+ requiresWrite: false,
545
+ annotations: {
546
+ readOnlyHint: true,
547
+ destructiveHint: false,
548
+ idempotentHint: true,
549
+ },
550
+ inputSchema: withUserAuth(z.object({
551
+ searchTerm: z.string()
552
+ .transform(val => val.trim())
553
+ .refine(val => val.length > 0, { message: 'Search term cannot be empty' })
554
+ .describe('Search term to find users by username or name'),
555
+ first: z.number().min(1).max(100).default(20).describe('Number of users to retrieve'),
556
+ })),
557
+ handler: async (input, client, userConfig) => {
558
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
559
+ const result = await client.searchUsers(input.searchTerm, input.first, credentials);
560
+ return result.users;
561
+ },
562
+ };
563
+ const searchGroupsTool = {
564
+ name: 'search_groups',
565
+ title: 'Search Groups',
566
+ description: 'Search for GitLab groups and organizations',
567
+ requiresAuth: false,
568
+ requiresWrite: false,
569
+ annotations: {
570
+ readOnlyHint: true,
571
+ destructiveHint: false,
572
+ idempotentHint: true,
573
+ },
574
+ inputSchema: withUserAuth(z.object({
575
+ searchTerm: z.string()
576
+ .transform(val => val.trim())
577
+ .refine(val => val.length > 0, { message: 'Search term cannot be empty' })
578
+ .describe('Search term to find groups by name or path'),
579
+ first: z.number().min(1).max(100).default(20).describe('Number of groups to retrieve'),
580
+ })),
581
+ handler: async (input, client, userConfig) => {
582
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
583
+ const result = await client.searchGroups(input.searchTerm, input.first, credentials);
584
+ return result.groups;
585
+ },
586
+ };
587
+ const browseRepositoryTool = {
588
+ name: 'browse_repository',
589
+ title: 'Browse Repository',
590
+ description: 'Browse repository files and folders - essential for exploring codebase structure',
591
+ requiresAuth: false,
592
+ requiresWrite: false,
593
+ annotations: {
594
+ readOnlyHint: true,
595
+ destructiveHint: false,
596
+ idempotentHint: true,
597
+ },
598
+ inputSchema: withUserAuth(z.object({
599
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
600
+ path: z.string().default('').describe('Directory path to browse (empty for root)'),
601
+ ref: z.string().default('HEAD').describe('Git reference (branch, tag, or commit SHA)'),
602
+ })),
603
+ handler: async (input, client, userConfig) => {
604
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
605
+ const result = await client.searchRepositoryFiles(input.projectPath, input.path, input.ref, credentials);
606
+ const projectWebUrl = result.project.webUrl;
607
+ const refParam = input.ref || 'HEAD';
608
+ const files = result.project.repository.tree.blobs.nodes.map((f) => ({
609
+ ...f,
610
+ webUrl: `${projectWebUrl}/-/blob/${refParam}/${f.path}`
611
+ }));
612
+ const directories = result.project.repository.tree.trees.nodes.map((d) => ({
613
+ ...d,
614
+ webUrl: `${projectWebUrl}/-/tree/${refParam}/${d.path}`
615
+ }));
616
+ return {
617
+ project: input.projectPath,
618
+ path: input.path,
619
+ ref: refParam,
620
+ files,
621
+ directories
622
+ };
623
+ },
624
+ };
625
+ const getFileContentTool = {
626
+ name: 'get_file_content',
627
+ title: 'File Content',
628
+ description: 'Get the content of a specific file from a GitLab repository - crucial for code analysis',
629
+ requiresAuth: false,
630
+ requiresWrite: false,
631
+ annotations: {
632
+ readOnlyHint: true,
633
+ destructiveHint: false,
634
+ idempotentHint: true,
635
+ },
636
+ inputSchema: withUserAuth(z.object({
637
+ projectPath: z.string().describe('Full path of the project (e.g., "group/project-name")'),
638
+ filePath: z.string().describe('Path to the file within the repository (e.g., "src/main.js")'),
639
+ ref: z.string().default('HEAD').describe('Git reference (branch, tag, or commit SHA)'),
640
+ })),
641
+ handler: async (input, client, userConfig) => {
642
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
643
+ const result = await client.getFileContent(input.projectPath, input.filePath, input.ref, credentials);
644
+ if (result.project.repository.blobs.nodes.length === 0) {
645
+ throw new Error(`File not found: ${input.filePath} in ${input.projectPath} at ${input.ref}`);
646
+ }
647
+ const file = result.project.repository.blobs.nodes[0];
648
+ const projectWebUrl = result.project.webUrl;
649
+ const refParam = input.ref || 'HEAD';
650
+ const fileWebUrl = `${projectWebUrl}/-/blob/${refParam}/${file.path}`;
651
+ return {
652
+ project: input.projectPath,
653
+ path: file.path,
654
+ name: file.name,
655
+ size: file.size,
656
+ content: file.rawBlob,
657
+ webUrl: fileWebUrl,
658
+ ref: refParam,
659
+ isLFS: !!file.lfsOid
660
+ };
661
+ },
662
+ };
663
+ // Helper functions for common user queries
664
+ const getUserIssuesTool = {
665
+ name: 'get_user_issues',
666
+ title: 'User Issues',
667
+ description: 'Get all issues assigned to a specific user - uses proper GraphQL filtering for reliable results',
668
+ requiresAuth: false,
669
+ requiresWrite: false,
670
+ annotations: {
671
+ readOnlyHint: true,
672
+ destructiveHint: false,
673
+ idempotentHint: true,
674
+ },
675
+ inputSchema: withUserAuth(z.object({
676
+ username: z.string().describe('Username to find issues for (e.g., "cdhanlon")'),
677
+ state: z.string().default('opened').describe('Filter by issue state (opened, closed, all)'),
678
+ 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'),
680
+ after: z.string().optional().describe('Cursor for pagination'),
681
+ })),
682
+ handler: async (input, client, userConfig) => {
683
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
684
+ // Use the searchIssues method with assigneeUsernames filter
685
+ const result = await client.searchIssues(undefined, // No text search
686
+ input.projectPath, input.state, input.first, input.after, credentials, [input.username], // assigneeUsernames
687
+ undefined, // authorUsername
688
+ undefined // labelNames
689
+ );
690
+ if (input.projectPath) {
691
+ if (!result || !result.project || !result.project.issues) {
692
+ throw new Error('Project not found or issues are not accessible for the provided path');
693
+ }
694
+ return {
695
+ username: input.username,
696
+ state: input.state,
697
+ projectPath: input.projectPath,
698
+ ...result.project.issues
699
+ };
700
+ }
701
+ else {
702
+ return {
703
+ username: input.username,
704
+ state: input.state,
705
+ ...result.issues
706
+ };
707
+ }
708
+ },
709
+ };
710
+ const getUserMergeRequestsTool = {
711
+ name: 'get_user_merge_requests',
712
+ title: 'User Merge Requests',
713
+ description: 'Get merge requests for a specific user (as author or assignee) - uses proper GraphQL filtering',
714
+ requiresAuth: false,
715
+ requiresWrite: false,
716
+ annotations: {
717
+ readOnlyHint: true,
718
+ destructiveHint: false,
719
+ idempotentHint: true,
720
+ },
721
+ inputSchema: withUserAuth(z.object({
722
+ username: z.string().describe('Username to find merge requests for (e.g., "cdhanlon")'),
723
+ role: z.enum(['author', 'assignee']).default('author').describe('Whether to find MRs authored by or assigned to the user'),
724
+ state: z.string().default('opened').describe('Filter by MR state (opened, closed, merged, all)'),
725
+ 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'),
727
+ after: z.string().optional().describe('Cursor for pagination'),
728
+ })),
729
+ handler: async (input, client, userConfig) => {
730
+ const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
731
+ // Use the searchMergeRequests method with author: or assignee: prefix
732
+ 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);
734
+ if (input.projectPath) {
735
+ if (!result || !result.project || !result.project.mergeRequests) {
736
+ throw new Error(`Project "${input.projectPath}" not found or merge requests are not accessible`);
737
+ }
738
+ return {
739
+ username: input.username,
740
+ role: input.role,
741
+ state: input.state,
742
+ projectPath: input.projectPath,
743
+ ...result.project.mergeRequests
744
+ };
745
+ }
746
+ return {
747
+ username: input.username,
748
+ role: input.role,
749
+ state: input.state,
750
+ ...result
751
+ };
752
+ },
753
+ };
754
+ export const searchTools = [
755
+ globalSearchTool,
756
+ searchProjectsTool,
757
+ searchIssuesTool,
758
+ searchMergeRequestsTool,
759
+ getUserIssuesTool,
760
+ getUserMergeRequestsTool,
761
+ searchUsersTool,
762
+ searchGroupsTool,
763
+ browseRepositoryTool,
764
+ getFileContentTool,
765
+ ];
766
+ export const tools = [
767
+ ...readOnlyTools,
768
+ ...userAuthTools,
769
+ ...writeTools,
770
+ updateIssueTool,
771
+ updateMergeRequestTool,
772
+ resolvePathTool,
773
+ getGroupProjectsTool,
774
+ getTypeFieldsTool,
775
+ ...searchTools,
776
+ ];
777
+ //# sourceMappingURL=tools.js.map