gitlab-mcp 0.1.4 → 1.0.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.
Files changed (55) hide show
  1. package/.dockerignore +7 -0
  2. package/.editorconfig +9 -0
  3. package/.env.example +75 -0
  4. package/.github/workflows/nodejs.yml +31 -0
  5. package/.github/workflows/npm-publish.yml +31 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.nvmrc +1 -0
  8. package/.prettierrc.json +6 -0
  9. package/Dockerfile +20 -0
  10. package/README.md +416 -251
  11. package/docker-compose.yml +10 -0
  12. package/docs/architecture.md +310 -0
  13. package/docs/authentication.md +299 -0
  14. package/docs/configuration.md +149 -0
  15. package/docs/deployment.md +336 -0
  16. package/docs/tools.md +294 -0
  17. package/eslint.config.js +23 -0
  18. package/package.json +70 -32
  19. package/scripts/get-oauth-token.example.sh +15 -0
  20. package/src/config/env.ts +171 -0
  21. package/src/http.ts +605 -0
  22. package/src/index.ts +77 -0
  23. package/src/lib/auth-context.ts +19 -0
  24. package/src/lib/gitlab-client.ts +1810 -0
  25. package/src/lib/logger.ts +17 -0
  26. package/src/lib/network.ts +45 -0
  27. package/src/lib/oauth.ts +287 -0
  28. package/src/lib/output.ts +51 -0
  29. package/src/lib/policy.ts +78 -0
  30. package/src/lib/request-runtime.ts +376 -0
  31. package/src/lib/sanitize.ts +25 -0
  32. package/src/server/build-server.ts +17 -0
  33. package/src/tools/gitlab.ts +3128 -0
  34. package/src/tools/health.ts +27 -0
  35. package/src/tools/mr-code-context.ts +473 -0
  36. package/src/types/context.ts +13 -0
  37. package/tests/auth-context.test.ts +102 -0
  38. package/tests/gitlab-client.test.ts +674 -0
  39. package/tests/graphql-guard.test.ts +121 -0
  40. package/tests/integration/agent-loop.integration.test.ts +552 -0
  41. package/tests/integration/server.integration.test.ts +543 -0
  42. package/tests/mr-code-context.test.ts +600 -0
  43. package/tests/oauth.test.ts +43 -0
  44. package/tests/output.test.ts +186 -0
  45. package/tests/policy.test.ts +324 -0
  46. package/tests/request-runtime.test.ts +252 -0
  47. package/tests/sanitize.test.ts +123 -0
  48. package/tests/upload-reference.test.ts +84 -0
  49. package/tsconfig.build.json +11 -0
  50. package/tsconfig.json +21 -0
  51. package/vitest.config.ts +12 -0
  52. package/LICENSE +0 -21
  53. package/build/index.js +0 -1641
  54. package/build/schemas.js +0 -684
  55. package/build/test-note.js +0 -54
@@ -0,0 +1,3128 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { z } from "zod";
4
+
5
+ import { GitLabApiError, type PushFileAction } from "../lib/gitlab-client.js";
6
+ import { getSessionAuth } from "../lib/auth-context.js";
7
+ import { stripNullsDeep } from "../lib/sanitize.js";
8
+ import type { AppContext } from "../types/context.js";
9
+ import { getMergeRequestCodeContext, mergeRequestCodeContextSchema } from "./mr-code-context.js";
10
+
11
+ type ToolArgs = Record<string, unknown>;
12
+
13
+ type ToolSchemaShape = Record<string, z.ZodTypeAny>;
14
+
15
+ interface GitLabToolDefinition {
16
+ name: string;
17
+ title: string;
18
+ description: string;
19
+ mutating: boolean;
20
+ requiresAuth?: boolean;
21
+ requiresFeature?: "wiki" | "milestone" | "pipeline" | "release";
22
+ inputSchema?: ToolSchemaShape;
23
+ handler: (args: ToolArgs, context: AppContext) => Promise<unknown>;
24
+ }
25
+
26
+ const optionalString = z.preprocess(
27
+ (value) => (value === null ? undefined : value),
28
+ z.string().optional()
29
+ );
30
+ const optionalNumber = z.preprocess(
31
+ (value) => (value === null ? undefined : value),
32
+ z.number().optional()
33
+ );
34
+ const optionalBoolean = z.preprocess(
35
+ (value) => (value === null ? undefined : value),
36
+ z.boolean().optional()
37
+ );
38
+ const optionalStringArray = z.preprocess(
39
+ (value) => (value === null ? undefined : value),
40
+ z.array(z.string()).optional()
41
+ );
42
+ const optionalNumberArray = z.preprocess(
43
+ (value) => (value === null ? undefined : value),
44
+ z.array(z.number()).optional()
45
+ );
46
+ const optionalStringOrNumber = z.preprocess(
47
+ (value) => (value === null ? undefined : value),
48
+ z.union([z.string(), z.number()]).optional()
49
+ );
50
+ const optionalStringOrStringArray = z.preprocess(
51
+ (value) => (value === null ? undefined : value),
52
+ z.union([z.string(), z.array(z.string())]).optional()
53
+ );
54
+ const optionalRecord = z.preprocess(
55
+ (value) => (value === null ? undefined : value),
56
+ z.record(z.string(), z.unknown()).optional()
57
+ );
58
+
59
+ const paginationShape = {
60
+ page: optionalNumber,
61
+ per_page: optionalNumber
62
+ } satisfies ToolSchemaShape;
63
+
64
+ export function registerGitLabTools(server: McpServer, context: AppContext): void {
65
+ const definitions = getGitLabToolDefinitions();
66
+ const disableGraphqlTools = shouldDisableGraphqlTools(
67
+ context.env.GITLAB_ALLOWED_PROJECT_IDS,
68
+ context.env.GITLAB_ALLOW_GRAPHQL_WITH_PROJECT_SCOPE
69
+ );
70
+ const filtered = context.policy.filterTools(
71
+ definitions.map((item) => ({
72
+ name: item.name,
73
+ mutating: item.mutating,
74
+ requiresFeature: item.requiresFeature
75
+ }))
76
+ );
77
+ const enabledNames = new Set(filtered.map((item) => item.name));
78
+
79
+ for (const definition of definitions) {
80
+ if (!enabledNames.has(definition.name)) {
81
+ continue;
82
+ }
83
+
84
+ if (disableGraphqlTools && isGraphqlToolName(definition.name)) {
85
+ continue;
86
+ }
87
+
88
+ server.registerTool(
89
+ definition.name,
90
+ {
91
+ title: definition.title,
92
+ description: definition.description,
93
+ inputSchema: definition.inputSchema ?? {}
94
+ },
95
+ async (rawArgs) => {
96
+ try {
97
+ context.policy.assertCanExecute({
98
+ name: definition.name,
99
+ mutating: definition.mutating,
100
+ requiresFeature: definition.requiresFeature
101
+ });
102
+
103
+ if (definition.requiresAuth ?? true) {
104
+ assertAuthReady(context);
105
+ }
106
+
107
+ const args = stripNullsDeep((rawArgs ?? {}) as ToolArgs);
108
+ const result = await definition.handler(args, context);
109
+ const formatted = context.formatter.format(result);
110
+
111
+ return {
112
+ content: [
113
+ {
114
+ type: "text",
115
+ text: formatted.text
116
+ }
117
+ ],
118
+ structuredContent: {
119
+ result: toStructuredContent(result),
120
+ meta: {
121
+ truncated: formatted.truncated,
122
+ bytes: formatted.bytes
123
+ }
124
+ }
125
+ };
126
+ } catch (error) {
127
+ return toToolError(error, context);
128
+ }
129
+ }
130
+ );
131
+ }
132
+ }
133
+
134
+ function getGitLabToolDefinitions(): GitLabToolDefinition[] {
135
+ return [
136
+ {
137
+ name: "gitlab_get_project",
138
+ title: "Get Project",
139
+ description: "Get project details by ID or path.",
140
+ mutating: false,
141
+ inputSchema: {
142
+ project_id: z.string().optional()
143
+ },
144
+ handler: async (args, context) => {
145
+ const projectId = resolveProjectId(args, context, true);
146
+ return context.gitlab.getProject(projectId);
147
+ }
148
+ },
149
+ {
150
+ name: "gitlab_list_projects",
151
+ title: "List Projects",
152
+ description: "List projects available to the current user.",
153
+ mutating: false,
154
+ inputSchema: {
155
+ search: optionalString,
156
+ search_namespaces: optionalBoolean,
157
+ membership: optionalBoolean,
158
+ owned: optionalBoolean,
159
+ simple: optionalBoolean,
160
+ archived: optionalBoolean,
161
+ visibility: z.enum(["public", "internal", "private"]).optional(),
162
+ order_by: z
163
+ .enum(["id", "name", "path", "created_at", "updated_at", "last_activity_at"])
164
+ .optional(),
165
+ sort: z.enum(["asc", "desc"]).optional(),
166
+ with_issues_enabled: optionalBoolean,
167
+ with_merge_requests_enabled: optionalBoolean,
168
+ min_access_level: optionalNumber,
169
+ ...paginationShape
170
+ },
171
+ handler: async (args, context) => context.gitlab.listProjects({ query: toQuery(args) })
172
+ },
173
+ {
174
+ name: "gitlab_create_repository",
175
+ title: "Create Repository",
176
+ description: "Create a new GitLab project/repository.",
177
+ mutating: true,
178
+ inputSchema: {
179
+ name: optionalString,
180
+ description: optionalString,
181
+ visibility: z.enum(["private", "internal", "public"]).optional(),
182
+ initialize_with_readme: optionalBoolean,
183
+ path: optionalString,
184
+ namespace_id: optionalString,
185
+ default_branch: optionalString
186
+ },
187
+ handler: async (args, context) =>
188
+ context.gitlab.createRepository({
189
+ name: getString(args, "name"),
190
+ description: getOptionalString(args, "description"),
191
+ visibility: getOptionalString(args, "visibility") as
192
+ | "private"
193
+ | "internal"
194
+ | "public"
195
+ | undefined,
196
+ initialize_with_readme: getOptionalBoolean(args, "initialize_with_readme"),
197
+ path: getOptionalString(args, "path"),
198
+ namespace_id: getOptionalString(args, "namespace_id"),
199
+ default_branch: getOptionalString(args, "default_branch")
200
+ })
201
+ },
202
+ {
203
+ name: "gitlab_list_project_members",
204
+ title: "List Project Members",
205
+ description: "List members of a project.",
206
+ mutating: false,
207
+ inputSchema: {
208
+ project_id: z.string().optional(),
209
+ query: optionalString,
210
+ user_ids: optionalNumberArray,
211
+ skip_users: optionalNumberArray,
212
+ include_inheritance: optionalBoolean,
213
+ ...paginationShape
214
+ },
215
+ handler: async (args, context) => {
216
+ const projectId = resolveProjectId(args, context, true);
217
+ return context.gitlab.listProjectMembers(projectId, {
218
+ query: toQuery(omit(args, ["project_id"]))
219
+ });
220
+ }
221
+ },
222
+ {
223
+ name: "gitlab_list_group_projects",
224
+ title: "List Group Projects",
225
+ description: "List projects under a group.",
226
+ mutating: false,
227
+ inputSchema: {
228
+ group_id: z.string(),
229
+ include_subgroups: optionalBoolean,
230
+ search: optionalString,
231
+ order_by: z
232
+ .enum(["name", "path", "created_at", "updated_at", "last_activity_at"])
233
+ .optional(),
234
+ sort: z.enum(["asc", "desc"]).optional(),
235
+ archived: optionalBoolean,
236
+ visibility: z.enum(["public", "internal", "private"]).optional(),
237
+ with_issues_enabled: optionalBoolean,
238
+ with_merge_requests_enabled: optionalBoolean,
239
+ min_access_level: optionalNumber,
240
+ with_programming_language: optionalString,
241
+ starred: optionalBoolean,
242
+ statistics: optionalBoolean,
243
+ with_custom_attributes: optionalBoolean,
244
+ with_security_reports: optionalBoolean,
245
+ ...paginationShape
246
+ },
247
+ handler: async (args, context) => {
248
+ return context.gitlab.listGroupProjects(getString(args, "group_id"), {
249
+ query: toQuery(omit(args, ["group_id"]))
250
+ });
251
+ }
252
+ },
253
+ {
254
+ name: "gitlab_list_group_iterations",
255
+ title: "List Group Iterations",
256
+ description: "List iterations for a group.",
257
+ mutating: false,
258
+ inputSchema: {
259
+ group_id: z.string().min(1),
260
+ state: optionalString,
261
+ search: optionalString,
262
+ search_in: optionalStringArray,
263
+ include_ancestors: optionalBoolean,
264
+ include_descendants: optionalBoolean,
265
+ updated_before: optionalString,
266
+ updated_after: optionalString,
267
+ ...paginationShape
268
+ },
269
+ handler: async (args, context) => {
270
+ const query = toQuery(omit(args, ["group_id"]));
271
+ const searchIn = getOptionalStringArray(args, "search_in");
272
+ if (searchIn && searchIn.length > 0) {
273
+ query.in = searchIn.join(",");
274
+ delete query.search_in;
275
+ }
276
+
277
+ return context.gitlab.listGroupIterations(getString(args, "group_id"), { query });
278
+ }
279
+ },
280
+ {
281
+ name: "gitlab_search_repositories",
282
+ title: "Search Repositories",
283
+ description: "Search repositories by keyword.",
284
+ mutating: false,
285
+ inputSchema: {
286
+ search: z.string().min(1),
287
+ ...paginationShape
288
+ },
289
+ handler: async (args, context) =>
290
+ context.gitlab.searchRepositories(getString(args, "search"), {
291
+ query: toQuery(omit(args, ["search"]))
292
+ })
293
+ },
294
+ {
295
+ name: "gitlab_search_code_blobs",
296
+ title: "Search Code Blobs",
297
+ description: "Search repository code blobs in a specific project.",
298
+ mutating: false,
299
+ inputSchema: {
300
+ project_id: z.string().optional(),
301
+ search: z.string().min(1),
302
+ ref: optionalString,
303
+ ...paginationShape
304
+ },
305
+ handler: async (args, context) =>
306
+ context.gitlab.searchCodeBlobs(
307
+ resolveProjectId(args, context, true),
308
+ getString(args, "search"),
309
+ { query: toQuery(omit(args, ["project_id", "search"])) }
310
+ )
311
+ },
312
+ {
313
+ name: "gitlab_get_repository_tree",
314
+ title: "Get Repository Tree",
315
+ description: "List files and directories in a repository tree.",
316
+ mutating: false,
317
+ inputSchema: {
318
+ project_id: z.string().optional(),
319
+ path: optionalString,
320
+ ref: optionalString,
321
+ recursive: optionalBoolean,
322
+ ...paginationShape
323
+ },
324
+ handler: async (args, context) => {
325
+ const projectId = resolveProjectId(args, context, true);
326
+ return context.gitlab.getRepositoryTree(projectId, {
327
+ query: toQuery(omit(args, ["project_id"]))
328
+ });
329
+ }
330
+ },
331
+ {
332
+ name: "gitlab_get_file_contents",
333
+ title: "Get File Contents",
334
+ description: "Get a file in repository by path and ref.",
335
+ mutating: false,
336
+ inputSchema: {
337
+ project_id: z.string().optional(),
338
+ file_path: z.string().min(1),
339
+ ref: optionalString
340
+ },
341
+ handler: async (args, context) => {
342
+ const projectId = resolveProjectId(args, context, true);
343
+ let ref = getOptionalString(args, "ref");
344
+ if (!ref) {
345
+ const project = (await context.gitlab.getProject(projectId)) as {
346
+ default_branch?: unknown;
347
+ };
348
+ ref = typeof project.default_branch === "string" ? project.default_branch : "main";
349
+ }
350
+ return context.gitlab.getFileContents(projectId, getString(args, "file_path"), ref);
351
+ }
352
+ },
353
+ {
354
+ name: "gitlab_create_or_update_file",
355
+ title: "Create Or Update File",
356
+ description: "Create or update one file in repository.",
357
+ mutating: true,
358
+ inputSchema: {
359
+ project_id: z.string().optional(),
360
+ file_path: z.string().min(1),
361
+ branch: z.string().min(1),
362
+ content: z.string(),
363
+ commit_message: z.string().min(1),
364
+ previous_path: optionalString,
365
+ author_email: optionalString,
366
+ author_name: optionalString,
367
+ encoding: optionalString,
368
+ execute_filemode: optionalBoolean,
369
+ start_branch: optionalString,
370
+ last_commit_id: optionalString,
371
+ commit_id: optionalString
372
+ },
373
+ handler: async (args, context) => {
374
+ const projectId = resolveProjectId(args, context, true);
375
+ return context.gitlab.createOrUpdateFile(projectId, getString(args, "file_path"), {
376
+ branch: getString(args, "branch"),
377
+ content: getString(args, "content"),
378
+ commit_message: getString(args, "commit_message"),
379
+ author_email: getOptionalString(args, "author_email"),
380
+ author_name: getOptionalString(args, "author_name"),
381
+ encoding: getOptionalString(args, "encoding") as "text" | "base64" | undefined,
382
+ execute_filemode: getOptionalBoolean(args, "execute_filemode"),
383
+ start_branch: getOptionalString(args, "start_branch"),
384
+ last_commit_id:
385
+ getOptionalString(args, "last_commit_id") ?? getOptionalString(args, "commit_id")
386
+ });
387
+ }
388
+ },
389
+ {
390
+ name: "gitlab_push_files",
391
+ title: "Push Files",
392
+ description: "Create a commit with multiple file actions.",
393
+ mutating: true,
394
+ inputSchema: {
395
+ project_id: z.string().optional(),
396
+ branch: z.string().min(1),
397
+ commit_message: z.string().min(1),
398
+ actions: z
399
+ .array(
400
+ z.object({
401
+ action: z.enum(["create", "delete", "move", "update", "chmod"]),
402
+ file_path: z.string(),
403
+ previous_path: optionalString,
404
+ content: optionalString,
405
+ encoding: optionalString,
406
+ execute_filemode: optionalBoolean,
407
+ last_commit_id: optionalString
408
+ })
409
+ )
410
+ .optional(),
411
+ files: z
412
+ .array(
413
+ z.object({
414
+ file_path: z.string(),
415
+ content: z.string()
416
+ })
417
+ )
418
+ .optional(),
419
+ start_branch: optionalString,
420
+ author_name: optionalString,
421
+ author_email: optionalString,
422
+ force: optionalBoolean
423
+ },
424
+ handler: async (args, context) => {
425
+ const projectId = resolveProjectId(args, context, true);
426
+ const actionsInput = args.actions;
427
+ const filesInput = args.files;
428
+
429
+ let actions: PushFileAction[] = [];
430
+ if (Array.isArray(actionsInput) && actionsInput.length > 0) {
431
+ actions = actionsInput as PushFileAction[];
432
+ } else if (Array.isArray(filesInput) && filesInput.length > 0) {
433
+ actions = filesInput.map((item) => {
434
+ const record = item as { file_path: string; content: string };
435
+ return {
436
+ action: "create",
437
+ file_path: record.file_path,
438
+ content: record.content
439
+ } satisfies PushFileAction;
440
+ });
441
+ }
442
+
443
+ if (actions.length === 0) {
444
+ throw new Error("Either actions or files must contain at least one item");
445
+ }
446
+
447
+ return context.gitlab.pushFiles(projectId, {
448
+ branch: getString(args, "branch"),
449
+ commit_message: getString(args, "commit_message"),
450
+ actions,
451
+ start_branch: getOptionalString(args, "start_branch"),
452
+ author_name: getOptionalString(args, "author_name"),
453
+ author_email: getOptionalString(args, "author_email"),
454
+ force: getOptionalBoolean(args, "force")
455
+ });
456
+ }
457
+ },
458
+ {
459
+ name: "gitlab_create_branch",
460
+ title: "Create Branch",
461
+ description: "Create a new branch from an existing ref.",
462
+ mutating: true,
463
+ inputSchema: {
464
+ project_id: z.string().optional(),
465
+ branch: z.string().min(1),
466
+ ref: optionalString
467
+ },
468
+ handler: async (args, context) => {
469
+ const projectId = resolveProjectId(args, context, true);
470
+ let ref = getOptionalString(args, "ref");
471
+
472
+ if (!ref) {
473
+ const project = (await context.gitlab.getProject(projectId)) as {
474
+ default_branch?: unknown;
475
+ };
476
+ ref = typeof project.default_branch === "string" ? project.default_branch : "main";
477
+ }
478
+
479
+ return context.gitlab.createBranch(projectId, {
480
+ branch: getString(args, "branch"),
481
+ ref
482
+ });
483
+ }
484
+ },
485
+ {
486
+ name: "gitlab_get_branch_diffs",
487
+ title: "Get Branch Diffs",
488
+ description: "Compare two branches/refs and return diffs.",
489
+ mutating: false,
490
+ inputSchema: {
491
+ project_id: z.string().optional(),
492
+ from: z.string().min(1),
493
+ to: z.string().min(1),
494
+ straight: optionalBoolean,
495
+ excluded_file_patterns: optionalStringArray
496
+ },
497
+ handler: async (args, context) => {
498
+ const projectId = resolveProjectId(args, context, true);
499
+ const query = toQuery({ excluded_file_patterns: args.excluded_file_patterns });
500
+ return context.gitlab.getBranchDiffs(
501
+ projectId,
502
+ {
503
+ from: getString(args, "from"),
504
+ to: getString(args, "to"),
505
+ straight: getOptionalBoolean(args, "straight")
506
+ },
507
+ {
508
+ query
509
+ }
510
+ );
511
+ }
512
+ },
513
+ {
514
+ name: "gitlab_list_commits",
515
+ title: "List Commits",
516
+ description: "List commits in a project.",
517
+ mutating: false,
518
+ inputSchema: {
519
+ project_id: z.string().optional(),
520
+ ref_name: optionalString,
521
+ since: optionalString,
522
+ until: optionalString,
523
+ path: optionalString,
524
+ author: optionalString,
525
+ all: optionalBoolean,
526
+ with_stats: optionalBoolean,
527
+ first_parent: optionalBoolean,
528
+ order: z.enum(["default", "topo"]).optional(),
529
+ trailers: optionalBoolean,
530
+ ...paginationShape
531
+ },
532
+ handler: async (args, context) => {
533
+ const projectId = resolveProjectId(args, context, true);
534
+ return context.gitlab.listCommits(projectId, {
535
+ query: toQuery(omit(args, ["project_id"]))
536
+ });
537
+ }
538
+ },
539
+ {
540
+ name: "gitlab_get_commit",
541
+ title: "Get Commit",
542
+ description: "Get one commit by SHA.",
543
+ mutating: false,
544
+ inputSchema: {
545
+ project_id: z.string().optional(),
546
+ sha: z.string().min(1),
547
+ stats: optionalBoolean
548
+ },
549
+ handler: async (args, context) => {
550
+ const projectId = resolveProjectId(args, context, true);
551
+ return context.gitlab.getCommit(projectId, getString(args, "sha"), {
552
+ query: toQuery(omit(args, ["project_id", "sha"]))
553
+ });
554
+ }
555
+ },
556
+ {
557
+ name: "gitlab_get_commit_diff",
558
+ title: "Get Commit Diff",
559
+ description: "Get diff for one commit.",
560
+ mutating: false,
561
+ inputSchema: {
562
+ project_id: z.string().optional(),
563
+ sha: z.string().min(1),
564
+ full_diff: optionalBoolean,
565
+ ...paginationShape
566
+ },
567
+ handler: async (args, context) => {
568
+ const projectId = resolveProjectId(args, context, true);
569
+ return context.gitlab.getCommitDiff(projectId, getString(args, "sha"), {
570
+ query: toQuery(omit(args, ["project_id", "sha"]))
571
+ });
572
+ }
573
+ },
574
+ {
575
+ name: "gitlab_list_merge_requests",
576
+ title: "List Merge Requests",
577
+ description: "List merge requests for a project.",
578
+ mutating: false,
579
+ inputSchema: {
580
+ project_id: z.string().optional(),
581
+ assignee_id: optionalStringOrNumber,
582
+ assignee_username: optionalString,
583
+ author_id: optionalStringOrNumber,
584
+ author_username: optionalString,
585
+ reviewer_id: optionalStringOrNumber,
586
+ reviewer_username: optionalString,
587
+ created_after: optionalString,
588
+ created_before: optionalString,
589
+ updated_after: optionalString,
590
+ updated_before: optionalString,
591
+ labels: optionalStringOrStringArray,
592
+ milestone: optionalString,
593
+ state: optionalString,
594
+ scope: optionalString,
595
+ order_by: z
596
+ .enum([
597
+ "created_at",
598
+ "updated_at",
599
+ "priority",
600
+ "label_priority",
601
+ "milestone_due",
602
+ "popularity"
603
+ ])
604
+ .optional(),
605
+ sort: z.enum(["asc", "desc"]).optional(),
606
+ source_branch: optionalString,
607
+ target_branch: optionalString,
608
+ search: optionalString,
609
+ wip: z.enum(["yes", "no"]).optional(),
610
+ with_labels_details: optionalBoolean,
611
+ ...paginationShape
612
+ },
613
+ handler: async (args, context) => {
614
+ const projectId = resolveProjectId(args, context, false);
615
+ const query = toQuery(omit(args, ["project_id"]));
616
+
617
+ if (projectId) {
618
+ return context.gitlab.listMergeRequests(projectId, { query });
619
+ }
620
+
621
+ return context.gitlab.listGlobalMergeRequests({ query });
622
+ }
623
+ },
624
+ {
625
+ name: "gitlab_get_merge_request",
626
+ title: "Get Merge Request",
627
+ description: "Get one merge request.",
628
+ mutating: false,
629
+ inputSchema: {
630
+ project_id: z.string().optional(),
631
+ merge_request_iid: optionalString,
632
+ source_branch: optionalString
633
+ },
634
+ handler: async (args, context) => {
635
+ const projectId = resolveProjectId(args, context, true);
636
+ const mergeRequestIid = getOptionalString(args, "merge_request_iid");
637
+
638
+ if (mergeRequestIid) {
639
+ return context.gitlab.getMergeRequest(projectId, mergeRequestIid);
640
+ }
641
+
642
+ const sourceBranch = getOptionalString(args, "source_branch");
643
+ if (!sourceBranch) {
644
+ throw new Error("Either merge_request_iid or source_branch must be provided");
645
+ }
646
+
647
+ const candidates = await context.gitlab.listMergeRequests(projectId, {
648
+ query: {
649
+ source_branch: sourceBranch,
650
+ per_page: 100,
651
+ page: 1
652
+ }
653
+ });
654
+ const match = pickFirstMergeRequest(candidates);
655
+ if (!match) {
656
+ throw new Error(`No merge request found for source_branch='${sourceBranch}'`);
657
+ }
658
+
659
+ return match;
660
+ }
661
+ },
662
+ {
663
+ name: "gitlab_create_merge_request",
664
+ title: "Create Merge Request",
665
+ description: "Create a merge request.",
666
+ mutating: true,
667
+ inputSchema: {
668
+ project_id: z.string().optional(),
669
+ source_branch: z.string().min(1),
670
+ target_branch: z.string().min(1),
671
+ title: z.string().min(1),
672
+ description: optionalString,
673
+ target_project_id: optionalString,
674
+ assignee_ids: optionalNumberArray,
675
+ reviewer_ids: optionalNumberArray,
676
+ labels: optionalStringOrStringArray,
677
+ allow_collaboration: optionalBoolean,
678
+ remove_source_branch: optionalBoolean,
679
+ squash: optionalBoolean,
680
+ draft: optionalBoolean
681
+ },
682
+ handler: async (args, context) => {
683
+ const projectId = resolveProjectId(args, context, true);
684
+ return context.gitlab.createMergeRequest(projectId, {
685
+ source_branch: getString(args, "source_branch"),
686
+ target_branch: getString(args, "target_branch"),
687
+ title: getString(args, "title"),
688
+ description: getOptionalString(args, "description"),
689
+ target_project_id: getOptionalString(args, "target_project_id"),
690
+ assignee_ids: getOptionalNumberArray(args, "assignee_ids"),
691
+ reviewer_ids: getOptionalNumberArray(args, "reviewer_ids"),
692
+ labels: toCsvValue(args.labels),
693
+ allow_collaboration: getOptionalBoolean(args, "allow_collaboration"),
694
+ remove_source_branch: getOptionalBoolean(args, "remove_source_branch"),
695
+ squash: getOptionalBoolean(args, "squash"),
696
+ draft: getOptionalBoolean(args, "draft")
697
+ });
698
+ }
699
+ },
700
+ {
701
+ name: "gitlab_fork_repository",
702
+ title: "Fork Repository",
703
+ description: "Fork an existing project to another namespace.",
704
+ mutating: true,
705
+ inputSchema: {
706
+ project_id: z.string().optional(),
707
+ namespace: optionalString,
708
+ namespace_id: optionalString,
709
+ path: optionalString,
710
+ name: optionalString,
711
+ description: optionalString,
712
+ visibility: z.enum(["private", "internal", "public"]).optional(),
713
+ default_branch: optionalString
714
+ },
715
+ handler: async (args, context) =>
716
+ context.gitlab.forkRepository(resolveProjectId(args, context, true), {
717
+ namespace: getOptionalString(args, "namespace"),
718
+ namespace_id: getOptionalString(args, "namespace_id"),
719
+ path: getOptionalString(args, "path"),
720
+ name: getOptionalString(args, "name"),
721
+ description: getOptionalString(args, "description"),
722
+ visibility: getOptionalString(args, "visibility") as
723
+ | "private"
724
+ | "internal"
725
+ | "public"
726
+ | undefined,
727
+ default_branch: getOptionalString(args, "default_branch")
728
+ })
729
+ },
730
+ {
731
+ name: "gitlab_update_merge_request",
732
+ title: "Update Merge Request",
733
+ description: "Update merge request fields.",
734
+ mutating: true,
735
+ inputSchema: {
736
+ project_id: z.string().optional(),
737
+ merge_request_iid: z.string().min(1),
738
+ source_branch: optionalString,
739
+ title: optionalString,
740
+ description: optionalString,
741
+ target_branch: optionalString,
742
+ assignee_ids: optionalNumberArray,
743
+ reviewer_ids: optionalNumberArray,
744
+ reviewers: optionalStringArray,
745
+ labels: optionalStringOrStringArray,
746
+ state_event: optionalString,
747
+ squash: optionalBoolean,
748
+ draft: optionalBoolean,
749
+ remove_source_branch: optionalBoolean
750
+ },
751
+ handler: async (args, context) => {
752
+ const projectId = resolveProjectId(args, context, true);
753
+ const payload = toQuery(omit(args, ["project_id", "merge_request_iid"])) as Record<
754
+ string,
755
+ unknown
756
+ >;
757
+ if (payload.labels === undefined) {
758
+ payload.labels = toCsvValue(args.labels);
759
+ }
760
+ if (Array.isArray(args.assignee_ids)) {
761
+ payload.assignee_ids = args.assignee_ids as number[];
762
+ }
763
+ if (Array.isArray(args.reviewer_ids)) {
764
+ payload.reviewer_ids = args.reviewer_ids as number[];
765
+ }
766
+ if (payload.reviewer_ids === undefined && Array.isArray(args.reviewers)) {
767
+ payload.reviewer_ids = (args.reviewers as string[]).join(",");
768
+ }
769
+ return context.gitlab.updateMergeRequest(
770
+ projectId,
771
+ getString(args, "merge_request_iid"),
772
+ payload
773
+ );
774
+ }
775
+ },
776
+ {
777
+ name: "gitlab_merge_merge_request",
778
+ title: "Merge Merge Request",
779
+ description: "Merge an existing merge request.",
780
+ mutating: true,
781
+ inputSchema: {
782
+ project_id: z.string().optional(),
783
+ merge_request_iid: optionalString,
784
+ source_branch: optionalString,
785
+ auto_merge: optionalBoolean,
786
+ merge_when_pipeline_succeeds: optionalBoolean,
787
+ merge_commit_message: optionalString,
788
+ squash_commit_message: optionalString,
789
+ should_remove_source_branch: optionalBoolean,
790
+ squash: optionalBoolean,
791
+ sha: optionalString
792
+ },
793
+ handler: async (args, context) => {
794
+ const projectId = resolveProjectId(args, context, true);
795
+ let mergeRequestIid = getOptionalString(args, "merge_request_iid");
796
+ if (!mergeRequestIid) {
797
+ const sourceBranch = getOptionalString(args, "source_branch");
798
+ if (!sourceBranch) {
799
+ throw new Error("Either merge_request_iid or source_branch must be provided");
800
+ }
801
+
802
+ const candidates = await context.gitlab.listMergeRequests(projectId, {
803
+ query: {
804
+ source_branch: sourceBranch,
805
+ per_page: 100,
806
+ page: 1
807
+ }
808
+ });
809
+ const match = pickFirstMergeRequest(candidates);
810
+ const iid = match?.iid;
811
+ if (typeof iid !== "number" && typeof iid !== "string") {
812
+ throw new Error(`No merge request found for source_branch='${sourceBranch}'`);
813
+ }
814
+ mergeRequestIid = String(iid);
815
+ }
816
+
817
+ return context.gitlab.mergeMergeRequest(
818
+ projectId,
819
+ mergeRequestIid,
820
+ toQuery(omit(args, ["project_id", "merge_request_iid", "source_branch"]))
821
+ );
822
+ }
823
+ },
824
+ {
825
+ name: "gitlab_get_merge_request_diffs",
826
+ title: "Get Merge Request Diffs",
827
+ description: "Get MR diffs with changed files.",
828
+ mutating: false,
829
+ inputSchema: {
830
+ project_id: z.string().optional(),
831
+ merge_request_iid: z.string().min(1),
832
+ view: z.enum(["inline", "parallel"]).optional(),
833
+ excluded_file_patterns: optionalStringArray
834
+ },
835
+ handler: async (args, context) =>
836
+ context.gitlab.getMergeRequestDiffs(
837
+ resolveProjectId(args, context, true),
838
+ getString(args, "merge_request_iid"),
839
+ { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) }
840
+ )
841
+ },
842
+ {
843
+ name: "gitlab_list_merge_request_diffs",
844
+ title: "List Merge Request Diffs",
845
+ description: "List detailed MR diffs (versions/changes view).",
846
+ mutating: false,
847
+ inputSchema: {
848
+ project_id: z.string().optional(),
849
+ merge_request_iid: z.string().min(1),
850
+ page: optionalNumber,
851
+ per_page: optionalNumber,
852
+ unidiff: optionalBoolean
853
+ },
854
+ handler: async (args, context) =>
855
+ context.gitlab.listMergeRequestDiffs(
856
+ resolveProjectId(args, context, true),
857
+ getString(args, "merge_request_iid"),
858
+ { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) }
859
+ )
860
+ },
861
+ {
862
+ name: "gitlab_get_merge_request_code_context",
863
+ title: "Get Merge Request Code Context",
864
+ description:
865
+ "High-signal MR code context with include/exclude filters, sorting, and token-budgeted output.",
866
+ mutating: false,
867
+ inputSchema: mergeRequestCodeContextSchema,
868
+ handler: async (args, context) =>
869
+ getMergeRequestCodeContext(
870
+ {
871
+ projectId: resolveProjectId(args, context, true),
872
+ mergeRequestIid: getString(args, "merge_request_iid"),
873
+ includePaths: getOptionalStringArray(args, "include_paths"),
874
+ excludePaths: getOptionalStringArray(args, "exclude_paths"),
875
+ extensions: getOptionalStringArray(args, "extensions"),
876
+ languages: getOptionalStringArray(args, "languages"),
877
+ maxFiles: getOptionalNumber(args, "max_files") ?? 30,
878
+ maxTotalChars: getOptionalNumber(args, "max_total_chars") ?? 120_000,
879
+ contextLines: getOptionalNumber(args, "context_lines") ?? 20,
880
+ mode:
881
+ (getOptionalString(args, "mode") as
882
+ | "patch"
883
+ | "surrounding"
884
+ | "fullfile"
885
+ | undefined) ?? "patch",
886
+ sort:
887
+ (getOptionalString(args, "sort") as
888
+ | "changed_lines"
889
+ | "path"
890
+ | "file_size"
891
+ | undefined) ?? "changed_lines",
892
+ listOnly: getOptionalBoolean(args, "list_only") ?? false
893
+ },
894
+ context
895
+ )
896
+ },
897
+ {
898
+ name: "gitlab_list_merge_request_versions",
899
+ title: "List Merge Request Versions",
900
+ description: "List MR diff versions.",
901
+ mutating: false,
902
+ inputSchema: {
903
+ project_id: z.string().optional(),
904
+ merge_request_iid: z.string().min(1)
905
+ },
906
+ handler: async (args, context) =>
907
+ context.gitlab.listMergeRequestVersions(
908
+ resolveProjectId(args, context, true),
909
+ getString(args, "merge_request_iid")
910
+ )
911
+ },
912
+ {
913
+ name: "gitlab_get_merge_request_version",
914
+ title: "Get Merge Request Version",
915
+ description: "Get one MR diff version.",
916
+ mutating: false,
917
+ inputSchema: {
918
+ project_id: z.string().optional(),
919
+ merge_request_iid: z.string().min(1),
920
+ version_id: z.string().min(1),
921
+ unidiff: optionalBoolean
922
+ },
923
+ handler: async (args, context) =>
924
+ context.gitlab.getMergeRequestVersion(
925
+ resolveProjectId(args, context, true),
926
+ getString(args, "merge_request_iid"),
927
+ getString(args, "version_id"),
928
+ { query: toQuery(omit(args, ["project_id", "merge_request_iid", "version_id"])) }
929
+ )
930
+ },
931
+ {
932
+ name: "gitlab_approve_merge_request",
933
+ title: "Approve Merge Request",
934
+ description: "Approve a merge request.",
935
+ mutating: true,
936
+ inputSchema: {
937
+ project_id: z.string().optional(),
938
+ merge_request_iid: z.string().min(1),
939
+ sha: optionalString,
940
+ approval_password: optionalString
941
+ },
942
+ handler: async (args, context) =>
943
+ context.gitlab.approveMergeRequest(
944
+ resolveProjectId(args, context, true),
945
+ getString(args, "merge_request_iid"),
946
+ toQuery(omit(args, ["project_id", "merge_request_iid"]))
947
+ )
948
+ },
949
+ {
950
+ name: "gitlab_unapprove_merge_request",
951
+ title: "Unapprove Merge Request",
952
+ description: "Remove current user approval from MR.",
953
+ mutating: true,
954
+ inputSchema: {
955
+ project_id: z.string().optional(),
956
+ merge_request_iid: z.string().min(1)
957
+ },
958
+ handler: async (args, context) =>
959
+ context.gitlab.unapproveMergeRequest(
960
+ resolveProjectId(args, context, true),
961
+ getString(args, "merge_request_iid")
962
+ )
963
+ },
964
+ {
965
+ name: "gitlab_get_merge_request_approval_state",
966
+ title: "Get Merge Request Approval State",
967
+ description: "Get approval state for MR.",
968
+ mutating: false,
969
+ inputSchema: {
970
+ project_id: z.string().optional(),
971
+ merge_request_iid: z.string().min(1)
972
+ },
973
+ handler: async (args, context) =>
974
+ context.gitlab.getMergeRequestApprovalState(
975
+ resolveProjectId(args, context, true),
976
+ getString(args, "merge_request_iid")
977
+ )
978
+ },
979
+ {
980
+ name: "gitlab_list_merge_request_discussions",
981
+ title: "List Merge Request Discussions",
982
+ description: "List MR discussions.",
983
+ mutating: false,
984
+ inputSchema: {
985
+ project_id: z.string().optional(),
986
+ merge_request_iid: z.string().min(1),
987
+ ...paginationShape
988
+ },
989
+ handler: async (args, context) =>
990
+ context.gitlab.listMergeRequestDiscussions(
991
+ resolveProjectId(args, context, true),
992
+ getString(args, "merge_request_iid"),
993
+ { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) }
994
+ )
995
+ },
996
+ {
997
+ name: "gitlab_create_merge_request_thread",
998
+ title: "Create Merge Request Thread",
999
+ description: "Create a new MR discussion thread (supports diff positions).",
1000
+ mutating: true,
1001
+ inputSchema: {
1002
+ project_id: z.string().optional(),
1003
+ merge_request_iid: z.string().min(1),
1004
+ body: z.string().min(1),
1005
+ position: optionalRecord,
1006
+ created_at: optionalString
1007
+ },
1008
+ handler: async (args, context) =>
1009
+ context.gitlab.createMergeRequestThread(
1010
+ resolveProjectId(args, context, true),
1011
+ getString(args, "merge_request_iid"),
1012
+ {
1013
+ body: getString(args, "body"),
1014
+ position: getOptionalRecord(args, "position"),
1015
+ created_at: getOptionalString(args, "created_at")
1016
+ }
1017
+ )
1018
+ },
1019
+ {
1020
+ name: "gitlab_mr_discussions",
1021
+ title: "Merge Request Discussions (Alias)",
1022
+ description: "Backward-compatible alias of gitlab_list_merge_request_discussions.",
1023
+ mutating: false,
1024
+ inputSchema: {
1025
+ project_id: z.string().optional(),
1026
+ merge_request_iid: z.string().min(1),
1027
+ ...paginationShape
1028
+ },
1029
+ handler: async (args, context) =>
1030
+ context.gitlab.listMergeRequestDiscussions(
1031
+ resolveProjectId(args, context, true),
1032
+ getString(args, "merge_request_iid"),
1033
+ { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) }
1034
+ )
1035
+ },
1036
+ {
1037
+ name: "gitlab_create_merge_request_discussion_note",
1038
+ title: "Create MR Discussion Note",
1039
+ description: "Add note to existing MR discussion thread.",
1040
+ mutating: true,
1041
+ inputSchema: {
1042
+ project_id: z.string().optional(),
1043
+ merge_request_iid: z.string().min(1),
1044
+ discussion_id: z.string().min(1),
1045
+ body: z.string().min(1),
1046
+ created_at: optionalString
1047
+ },
1048
+ handler: async (args, context) =>
1049
+ context.gitlab.createMergeRequestDiscussionNote(
1050
+ resolveProjectId(args, context, true),
1051
+ getString(args, "merge_request_iid"),
1052
+ getString(args, "discussion_id"),
1053
+ {
1054
+ body: getString(args, "body"),
1055
+ created_at: getOptionalString(args, "created_at")
1056
+ }
1057
+ )
1058
+ },
1059
+ {
1060
+ name: "gitlab_update_merge_request_discussion_note",
1061
+ title: "Update MR Discussion Note",
1062
+ description: "Update note body/resolved state in MR discussion.",
1063
+ mutating: true,
1064
+ inputSchema: {
1065
+ project_id: z.string().optional(),
1066
+ merge_request_iid: z.string().min(1),
1067
+ discussion_id: z.string().min(1),
1068
+ note_id: z.string().min(1),
1069
+ body: optionalString,
1070
+ resolved: optionalBoolean
1071
+ },
1072
+ handler: async (args, context) => {
1073
+ const body = getOptionalString(args, "body");
1074
+ const resolved = getOptionalBoolean(args, "resolved");
1075
+
1076
+ if (body === undefined && resolved === undefined) {
1077
+ throw new Error("Either body or resolved must be provided");
1078
+ }
1079
+
1080
+ if (body !== undefined && resolved !== undefined) {
1081
+ throw new Error("Provide either body or resolved, not both");
1082
+ }
1083
+
1084
+ return context.gitlab.updateMergeRequestDiscussionNote(
1085
+ resolveProjectId(args, context, true),
1086
+ getString(args, "merge_request_iid"),
1087
+ getString(args, "discussion_id"),
1088
+ getString(args, "note_id"),
1089
+ {
1090
+ body,
1091
+ resolved
1092
+ }
1093
+ );
1094
+ }
1095
+ },
1096
+ {
1097
+ name: "gitlab_delete_merge_request_discussion_note",
1098
+ title: "Delete MR Discussion Note",
1099
+ description: "Delete note from MR discussion thread.",
1100
+ mutating: true,
1101
+ inputSchema: {
1102
+ project_id: z.string().optional(),
1103
+ merge_request_iid: z.string().min(1),
1104
+ discussion_id: z.string().min(1),
1105
+ note_id: z.string().min(1)
1106
+ },
1107
+ handler: async (args, context) =>
1108
+ context.gitlab.deleteMergeRequestDiscussionNote(
1109
+ resolveProjectId(args, context, true),
1110
+ getString(args, "merge_request_iid"),
1111
+ getString(args, "discussion_id"),
1112
+ getString(args, "note_id")
1113
+ )
1114
+ },
1115
+ {
1116
+ name: "gitlab_resolve_merge_request_thread",
1117
+ title: "Resolve Merge Request Thread",
1118
+ description: "Resolve/unresolve an MR discussion note.",
1119
+ mutating: true,
1120
+ inputSchema: {
1121
+ project_id: z.string().optional(),
1122
+ merge_request_iid: z.string().min(1),
1123
+ discussion_id: z.string().min(1),
1124
+ note_id: z.string().min(1),
1125
+ resolved: z.boolean().default(true)
1126
+ },
1127
+ handler: async (args, context) =>
1128
+ context.gitlab.resolveMergeRequestThread(
1129
+ resolveProjectId(args, context, true),
1130
+ getString(args, "merge_request_iid"),
1131
+ getString(args, "discussion_id"),
1132
+ getString(args, "note_id"),
1133
+ getBoolean(args, "resolved")
1134
+ )
1135
+ },
1136
+ {
1137
+ name: "gitlab_list_merge_request_notes",
1138
+ title: "List Merge Request Notes",
1139
+ description: "List top-level notes for an MR.",
1140
+ mutating: false,
1141
+ inputSchema: {
1142
+ project_id: z.string().optional(),
1143
+ merge_request_iid: z.string().min(1),
1144
+ sort: optionalString,
1145
+ order_by: optionalString,
1146
+ ...paginationShape
1147
+ },
1148
+ handler: async (args, context) =>
1149
+ context.gitlab.listMergeRequestNotes(
1150
+ resolveProjectId(args, context, true),
1151
+ getString(args, "merge_request_iid"),
1152
+ { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) }
1153
+ )
1154
+ },
1155
+ {
1156
+ name: "gitlab_get_merge_request_notes",
1157
+ title: "Get Merge Request Notes (Alias)",
1158
+ description: "Backward-compatible alias of gitlab_list_merge_request_notes.",
1159
+ mutating: false,
1160
+ inputSchema: {
1161
+ project_id: z.string().optional(),
1162
+ merge_request_iid: z.string().min(1),
1163
+ sort: optionalString,
1164
+ order_by: optionalString,
1165
+ ...paginationShape
1166
+ },
1167
+ handler: async (args, context) =>
1168
+ context.gitlab.listMergeRequestNotes(
1169
+ resolveProjectId(args, context, true),
1170
+ getString(args, "merge_request_iid"),
1171
+ { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) }
1172
+ )
1173
+ },
1174
+ {
1175
+ name: "gitlab_get_draft_note",
1176
+ title: "Get Draft Note",
1177
+ description: "Get a single merge-request draft note.",
1178
+ mutating: false,
1179
+ inputSchema: {
1180
+ project_id: z.string().optional(),
1181
+ merge_request_iid: z.string().min(1),
1182
+ draft_note_id: z.string().min(1)
1183
+ },
1184
+ handler: async (args, context) =>
1185
+ context.gitlab.getDraftNote(
1186
+ resolveProjectId(args, context, true),
1187
+ getString(args, "merge_request_iid"),
1188
+ getString(args, "draft_note_id")
1189
+ )
1190
+ },
1191
+ {
1192
+ name: "gitlab_list_draft_notes",
1193
+ title: "List Draft Notes",
1194
+ description: "List draft notes on a merge request.",
1195
+ mutating: false,
1196
+ inputSchema: {
1197
+ project_id: z.string().optional(),
1198
+ merge_request_iid: z.string().min(1)
1199
+ },
1200
+ handler: async (args, context) =>
1201
+ context.gitlab.listDraftNotes(
1202
+ resolveProjectId(args, context, true),
1203
+ getString(args, "merge_request_iid")
1204
+ )
1205
+ },
1206
+ {
1207
+ name: "gitlab_create_draft_note",
1208
+ title: "Create Draft Note",
1209
+ description: "Create a merge-request draft note.",
1210
+ mutating: true,
1211
+ inputSchema: {
1212
+ project_id: z.string().optional(),
1213
+ merge_request_iid: z.string().min(1),
1214
+ body: z.string().min(1),
1215
+ position: optionalRecord,
1216
+ resolve_discussion: optionalBoolean
1217
+ },
1218
+ handler: async (args, context) =>
1219
+ context.gitlab.createDraftNote(
1220
+ resolveProjectId(args, context, true),
1221
+ getString(args, "merge_request_iid"),
1222
+ {
1223
+ body: getString(args, "body"),
1224
+ position: getOptionalRecord(args, "position"),
1225
+ resolve_discussion: getOptionalBoolean(args, "resolve_discussion")
1226
+ }
1227
+ )
1228
+ },
1229
+ {
1230
+ name: "gitlab_update_draft_note",
1231
+ title: "Update Draft Note",
1232
+ description: "Update a merge-request draft note.",
1233
+ mutating: true,
1234
+ inputSchema: {
1235
+ project_id: z.string().optional(),
1236
+ merge_request_iid: z.string().min(1),
1237
+ draft_note_id: z.string().min(1),
1238
+ body: optionalString,
1239
+ position: optionalRecord,
1240
+ resolve_discussion: optionalBoolean
1241
+ },
1242
+ handler: async (args, context) => {
1243
+ if (
1244
+ getOptionalString(args, "body") === undefined &&
1245
+ getOptionalRecord(args, "position") === undefined &&
1246
+ getOptionalBoolean(args, "resolve_discussion") === undefined
1247
+ ) {
1248
+ throw new Error("At least one of body, position, or resolve_discussion is required");
1249
+ }
1250
+
1251
+ return context.gitlab.updateDraftNote(
1252
+ resolveProjectId(args, context, true),
1253
+ getString(args, "merge_request_iid"),
1254
+ getString(args, "draft_note_id"),
1255
+ {
1256
+ body: getOptionalString(args, "body"),
1257
+ position: getOptionalRecord(args, "position"),
1258
+ resolve_discussion: getOptionalBoolean(args, "resolve_discussion")
1259
+ }
1260
+ );
1261
+ }
1262
+ },
1263
+ {
1264
+ name: "gitlab_delete_draft_note",
1265
+ title: "Delete Draft Note",
1266
+ description: "Delete a merge-request draft note.",
1267
+ mutating: true,
1268
+ inputSchema: {
1269
+ project_id: z.string().optional(),
1270
+ merge_request_iid: z.string().min(1),
1271
+ draft_note_id: z.string().min(1)
1272
+ },
1273
+ handler: async (args, context) =>
1274
+ context.gitlab.deleteDraftNote(
1275
+ resolveProjectId(args, context, true),
1276
+ getString(args, "merge_request_iid"),
1277
+ getString(args, "draft_note_id")
1278
+ )
1279
+ },
1280
+ {
1281
+ name: "gitlab_publish_draft_note",
1282
+ title: "Publish Draft Note",
1283
+ description: "Publish one merge-request draft note.",
1284
+ mutating: true,
1285
+ inputSchema: {
1286
+ project_id: z.string().optional(),
1287
+ merge_request_iid: z.string().min(1),
1288
+ draft_note_id: z.string().min(1)
1289
+ },
1290
+ handler: async (args, context) =>
1291
+ context.gitlab.publishDraftNote(
1292
+ resolveProjectId(args, context, true),
1293
+ getString(args, "merge_request_iid"),
1294
+ getString(args, "draft_note_id")
1295
+ )
1296
+ },
1297
+ {
1298
+ name: "gitlab_bulk_publish_draft_notes",
1299
+ title: "Bulk Publish Draft Notes",
1300
+ description: "Publish all merge-request draft notes.",
1301
+ mutating: true,
1302
+ inputSchema: {
1303
+ project_id: z.string().optional(),
1304
+ merge_request_iid: z.string().min(1)
1305
+ },
1306
+ handler: async (args, context) =>
1307
+ context.gitlab.bulkPublishDraftNotes(
1308
+ resolveProjectId(args, context, true),
1309
+ getString(args, "merge_request_iid")
1310
+ )
1311
+ },
1312
+ {
1313
+ name: "gitlab_get_merge_request_note",
1314
+ title: "Get Merge Request Note",
1315
+ description: "Get a single MR note.",
1316
+ mutating: false,
1317
+ inputSchema: {
1318
+ project_id: z.string().optional(),
1319
+ merge_request_iid: z.string().min(1),
1320
+ note_id: z.string().min(1)
1321
+ },
1322
+ handler: async (args, context) =>
1323
+ context.gitlab.getMergeRequestNote(
1324
+ resolveProjectId(args, context, true),
1325
+ getString(args, "merge_request_iid"),
1326
+ getString(args, "note_id")
1327
+ )
1328
+ },
1329
+ {
1330
+ name: "gitlab_create_merge_request_note",
1331
+ title: "Create Merge Request Note",
1332
+ description: "Create a top-level MR note.",
1333
+ mutating: true,
1334
+ inputSchema: {
1335
+ project_id: z.string().optional(),
1336
+ merge_request_iid: z.string().min(1),
1337
+ body: z.string().min(1)
1338
+ },
1339
+ handler: async (args, context) =>
1340
+ context.gitlab.createMergeRequestNote(
1341
+ resolveProjectId(args, context, true),
1342
+ getString(args, "merge_request_iid"),
1343
+ getString(args, "body")
1344
+ )
1345
+ },
1346
+ {
1347
+ name: "gitlab_create_note",
1348
+ title: "Create Note",
1349
+ description: "Create a note on an issue or merge request.",
1350
+ mutating: true,
1351
+ inputSchema: {
1352
+ project_id: z.string().optional(),
1353
+ noteable_type: z.enum(["issue", "merge_request"]),
1354
+ noteable_iid: z.string().min(1),
1355
+ body: z.string().min(1)
1356
+ },
1357
+ handler: async (args, context) =>
1358
+ context.gitlab.createNote(
1359
+ resolveProjectId(args, context, true),
1360
+ getString(args, "noteable_type") as "issue" | "merge_request",
1361
+ getString(args, "noteable_iid"),
1362
+ getString(args, "body")
1363
+ )
1364
+ },
1365
+ {
1366
+ name: "gitlab_update_merge_request_note",
1367
+ title: "Update Merge Request Note",
1368
+ description: "Update MR note body.",
1369
+ mutating: true,
1370
+ inputSchema: {
1371
+ project_id: z.string().optional(),
1372
+ merge_request_iid: z.string().min(1),
1373
+ note_id: z.string().min(1),
1374
+ body: z.string().min(1)
1375
+ },
1376
+ handler: async (args, context) =>
1377
+ context.gitlab.updateMergeRequestNote(
1378
+ resolveProjectId(args, context, true),
1379
+ getString(args, "merge_request_iid"),
1380
+ getString(args, "note_id"),
1381
+ getString(args, "body")
1382
+ )
1383
+ },
1384
+ {
1385
+ name: "gitlab_delete_merge_request_note",
1386
+ title: "Delete Merge Request Note",
1387
+ description: "Delete an MR note.",
1388
+ mutating: true,
1389
+ inputSchema: {
1390
+ project_id: z.string().optional(),
1391
+ merge_request_iid: z.string().min(1),
1392
+ note_id: z.string().min(1)
1393
+ },
1394
+ handler: async (args, context) =>
1395
+ context.gitlab.deleteMergeRequestNote(
1396
+ resolveProjectId(args, context, true),
1397
+ getString(args, "merge_request_iid"),
1398
+ getString(args, "note_id")
1399
+ )
1400
+ },
1401
+ {
1402
+ name: "gitlab_list_issues",
1403
+ title: "List Issues",
1404
+ description: "List issues in project.",
1405
+ mutating: false,
1406
+ inputSchema: {
1407
+ project_id: z.string().optional(),
1408
+ assignee_id: optionalStringOrNumber,
1409
+ assignee_username: optionalStringArray,
1410
+ author_id: optionalStringOrNumber,
1411
+ author_username: optionalString,
1412
+ confidential: optionalBoolean,
1413
+ created_after: optionalString,
1414
+ created_before: optionalString,
1415
+ due_date: optionalString,
1416
+ labels: optionalStringOrStringArray,
1417
+ milestone: optionalString,
1418
+ issue_type: z.enum(["issue", "incident", "test_case", "task"]).optional(),
1419
+ iteration_id: optionalStringOrNumber,
1420
+ scope: z.enum(["created_by_me", "assigned_to_me", "all"]).optional(),
1421
+ state: optionalString,
1422
+ search: optionalString,
1423
+ updated_after: optionalString,
1424
+ updated_before: optionalString,
1425
+ with_labels_details: optionalBoolean,
1426
+ ...paginationShape
1427
+ },
1428
+ handler: async (args, context) => {
1429
+ const projectId = resolveProjectId(args, context, false);
1430
+ const query = toQuery(omit(args, ["project_id"]));
1431
+
1432
+ if (projectId) {
1433
+ return context.gitlab.listIssues(projectId, { query });
1434
+ }
1435
+
1436
+ return context.gitlab.listGlobalIssues({ query });
1437
+ }
1438
+ },
1439
+ {
1440
+ name: "gitlab_my_issues",
1441
+ title: "My Issues",
1442
+ description: "List issues assigned to the current authenticated user.",
1443
+ mutating: false,
1444
+ inputSchema: {
1445
+ project_id: z.string().optional(),
1446
+ state: z.enum(["opened", "closed", "all"]).optional(),
1447
+ labels: optionalStringOrStringArray,
1448
+ milestone: optionalString,
1449
+ search: optionalString,
1450
+ created_after: optionalString,
1451
+ created_before: optionalString,
1452
+ updated_after: optionalString,
1453
+ updated_before: optionalString,
1454
+ ...paginationShape
1455
+ },
1456
+ handler: async (args, context) => {
1457
+ const projectId = resolveProjectId(args, context, false);
1458
+ return context.gitlab.myIssues({
1459
+ project_id: projectId || undefined,
1460
+ ...(toQuery(omit(args, ["project_id"])) as Record<string, string | number | boolean>)
1461
+ });
1462
+ }
1463
+ },
1464
+ {
1465
+ name: "gitlab_get_issue",
1466
+ title: "Get Issue",
1467
+ description: "Get issue by IID.",
1468
+ mutating: false,
1469
+ inputSchema: {
1470
+ project_id: z.string().optional(),
1471
+ issue_iid: z.string().min(1)
1472
+ },
1473
+ handler: async (args, context) =>
1474
+ context.gitlab.getIssue(resolveProjectId(args, context, true), getString(args, "issue_iid"))
1475
+ },
1476
+ {
1477
+ name: "gitlab_create_issue",
1478
+ title: "Create Issue",
1479
+ description: "Create a new issue.",
1480
+ mutating: true,
1481
+ inputSchema: {
1482
+ project_id: z.string().optional(),
1483
+ title: z.string().min(1),
1484
+ description: optionalString,
1485
+ labels: optionalStringOrStringArray,
1486
+ milestone_id: optionalNumber,
1487
+ due_date: optionalString,
1488
+ confidential: optionalBoolean,
1489
+ issue_type: optionalString,
1490
+ assignee_ids: optionalNumberArray
1491
+ },
1492
+ handler: async (args, context) =>
1493
+ context.gitlab.createIssue(resolveProjectId(args, context, true), {
1494
+ title: getString(args, "title"),
1495
+ description: getOptionalString(args, "description"),
1496
+ labels: toCsvValue(args.labels),
1497
+ milestone_id: getOptionalNumber(args, "milestone_id"),
1498
+ due_date: getOptionalString(args, "due_date"),
1499
+ confidential: getOptionalBoolean(args, "confidential"),
1500
+ issue_type: getOptionalString(args, "issue_type"),
1501
+ assignee_ids: getOptionalNumberArray(args, "assignee_ids")
1502
+ })
1503
+ },
1504
+ {
1505
+ name: "gitlab_update_issue",
1506
+ title: "Update Issue",
1507
+ description: "Update issue fields.",
1508
+ mutating: true,
1509
+ inputSchema: {
1510
+ project_id: z.string().optional(),
1511
+ issue_iid: z.string().min(1),
1512
+ title: optionalString,
1513
+ description: optionalString,
1514
+ state_event: optionalString,
1515
+ labels: optionalStringOrStringArray,
1516
+ milestone_id: optionalNumber,
1517
+ due_date: optionalString,
1518
+ confidential: optionalBoolean,
1519
+ assignee_ids: optionalNumberArray,
1520
+ discussion_locked: optionalBoolean,
1521
+ weight: optionalNumber,
1522
+ issue_type: z.enum(["issue", "incident", "test_case", "task"]).optional()
1523
+ },
1524
+ handler: async (args, context) => {
1525
+ const payload = toQuery(omit(args, ["project_id", "issue_iid"])) as Record<string, unknown>;
1526
+ if (payload.labels === undefined) {
1527
+ payload.labels = toCsvValue(args.labels);
1528
+ }
1529
+ if (Array.isArray(args.assignee_ids)) {
1530
+ payload.assignee_ids = args.assignee_ids as number[];
1531
+ }
1532
+
1533
+ return context.gitlab.updateIssue(
1534
+ resolveProjectId(args, context, true),
1535
+ getString(args, "issue_iid"),
1536
+ payload
1537
+ );
1538
+ }
1539
+ },
1540
+ {
1541
+ name: "gitlab_delete_issue",
1542
+ title: "Delete Issue",
1543
+ description: "Delete an issue.",
1544
+ mutating: true,
1545
+ inputSchema: {
1546
+ project_id: z.string().optional(),
1547
+ issue_iid: z.string().min(1)
1548
+ },
1549
+ handler: async (args, context) =>
1550
+ context.gitlab.deleteIssue(
1551
+ resolveProjectId(args, context, true),
1552
+ getString(args, "issue_iid")
1553
+ )
1554
+ },
1555
+ {
1556
+ name: "gitlab_list_issue_discussions",
1557
+ title: "List Issue Discussions",
1558
+ description: "List issue discussions.",
1559
+ mutating: false,
1560
+ inputSchema: {
1561
+ project_id: z.string().optional(),
1562
+ issue_iid: z.string().min(1),
1563
+ ...paginationShape
1564
+ },
1565
+ handler: async (args, context) =>
1566
+ context.gitlab.listIssueDiscussions(
1567
+ resolveProjectId(args, context, true),
1568
+ getString(args, "issue_iid"),
1569
+ { query: toQuery(omit(args, ["project_id", "issue_iid"])) }
1570
+ )
1571
+ },
1572
+ {
1573
+ name: "gitlab_create_issue_note",
1574
+ title: "Create Issue Note",
1575
+ description: "Create issue comment (top-level or discussion note).",
1576
+ mutating: true,
1577
+ inputSchema: {
1578
+ project_id: z.string().optional(),
1579
+ issue_iid: z.string().min(1),
1580
+ discussion_id: optionalString,
1581
+ body: z.string().min(1),
1582
+ created_at: optionalString
1583
+ },
1584
+ handler: async (args, context) =>
1585
+ context.gitlab.createIssueNote(
1586
+ resolveProjectId(args, context, true),
1587
+ getString(args, "issue_iid"),
1588
+ {
1589
+ body: getString(args, "body"),
1590
+ discussion_id: getOptionalString(args, "discussion_id"),
1591
+ created_at: getOptionalString(args, "created_at")
1592
+ }
1593
+ )
1594
+ },
1595
+ {
1596
+ name: "gitlab_update_issue_note",
1597
+ title: "Update Issue Note",
1598
+ description: "Update an issue discussion note body or resolved state.",
1599
+ mutating: true,
1600
+ inputSchema: {
1601
+ project_id: z.string().optional(),
1602
+ issue_iid: z.string().min(1),
1603
+ discussion_id: z.string().min(1),
1604
+ note_id: z.string().min(1),
1605
+ body: optionalString,
1606
+ resolved: optionalBoolean
1607
+ },
1608
+ handler: async (args, context) => {
1609
+ const body = getOptionalString(args, "body");
1610
+ const resolved = getOptionalBoolean(args, "resolved");
1611
+
1612
+ if (body === undefined && resolved === undefined) {
1613
+ throw new Error("Either body or resolved must be provided");
1614
+ }
1615
+
1616
+ if (body !== undefined && resolved !== undefined) {
1617
+ throw new Error("Provide either body or resolved, not both");
1618
+ }
1619
+
1620
+ return context.gitlab.updateIssueNote(
1621
+ resolveProjectId(args, context, true),
1622
+ getString(args, "issue_iid"),
1623
+ getString(args, "discussion_id"),
1624
+ getString(args, "note_id"),
1625
+ { body, resolved }
1626
+ );
1627
+ }
1628
+ },
1629
+ {
1630
+ name: "gitlab_list_issue_links",
1631
+ title: "List Issue Links",
1632
+ description: "List related issue links for an issue.",
1633
+ mutating: false,
1634
+ inputSchema: {
1635
+ project_id: z.string().optional(),
1636
+ issue_iid: z.string().min(1)
1637
+ },
1638
+ handler: async (args, context) =>
1639
+ context.gitlab.listIssueLinks(
1640
+ resolveProjectId(args, context, true),
1641
+ getString(args, "issue_iid")
1642
+ )
1643
+ },
1644
+ {
1645
+ name: "gitlab_get_issue_link",
1646
+ title: "Get Issue Link",
1647
+ description: "Get a single issue link by ID.",
1648
+ mutating: false,
1649
+ inputSchema: {
1650
+ project_id: z.string().optional(),
1651
+ issue_iid: z.string().min(1),
1652
+ issue_link_id: z.string().min(1)
1653
+ },
1654
+ handler: async (args, context) =>
1655
+ context.gitlab.getIssueLink(
1656
+ resolveProjectId(args, context, true),
1657
+ getString(args, "issue_iid"),
1658
+ getString(args, "issue_link_id")
1659
+ )
1660
+ },
1661
+ {
1662
+ name: "gitlab_create_issue_link",
1663
+ title: "Create Issue Link",
1664
+ description: "Create a relation between two issues.",
1665
+ mutating: true,
1666
+ inputSchema: {
1667
+ project_id: z.string().optional(),
1668
+ issue_iid: z.string().min(1),
1669
+ target_project_id: z.string().min(1),
1670
+ target_issue_iid: z.string().min(1),
1671
+ link_type: z.enum(["relates_to", "blocks", "is_blocked_by"]).optional()
1672
+ },
1673
+ handler: async (args, context) =>
1674
+ context.gitlab.createIssueLink(
1675
+ resolveProjectId(args, context, true),
1676
+ getString(args, "issue_iid"),
1677
+ {
1678
+ target_project_id: getString(args, "target_project_id"),
1679
+ target_issue_iid: getString(args, "target_issue_iid"),
1680
+ link_type: getOptionalString(args, "link_type") as
1681
+ | "relates_to"
1682
+ | "blocks"
1683
+ | "is_blocked_by"
1684
+ | undefined
1685
+ }
1686
+ )
1687
+ },
1688
+ {
1689
+ name: "gitlab_delete_issue_link",
1690
+ title: "Delete Issue Link",
1691
+ description: "Delete a relation between issues.",
1692
+ mutating: true,
1693
+ inputSchema: {
1694
+ project_id: z.string().optional(),
1695
+ issue_iid: z.string().min(1),
1696
+ issue_link_id: z.string().min(1)
1697
+ },
1698
+ handler: async (args, context) =>
1699
+ context.gitlab.deleteIssueLink(
1700
+ resolveProjectId(args, context, true),
1701
+ getString(args, "issue_iid"),
1702
+ getString(args, "issue_link_id")
1703
+ )
1704
+ },
1705
+ {
1706
+ name: "gitlab_list_wiki_pages",
1707
+ title: "List Wiki Pages",
1708
+ description: "List wiki pages in a project.",
1709
+ mutating: false,
1710
+ requiresFeature: "wiki",
1711
+ inputSchema: {
1712
+ project_id: z.string().optional(),
1713
+ with_content: optionalBoolean,
1714
+ ...paginationShape
1715
+ },
1716
+ handler: async (args, context) =>
1717
+ context.gitlab.listWikiPages(resolveProjectId(args, context, true), {
1718
+ query: toQuery(omit(args, ["project_id"]))
1719
+ })
1720
+ },
1721
+ {
1722
+ name: "gitlab_get_wiki_page",
1723
+ title: "Get Wiki Page",
1724
+ description: "Get wiki page by slug.",
1725
+ mutating: false,
1726
+ requiresFeature: "wiki",
1727
+ inputSchema: {
1728
+ project_id: z.string().optional(),
1729
+ slug: z.string().min(1),
1730
+ version: optionalString
1731
+ },
1732
+ handler: async (args, context) =>
1733
+ context.gitlab.getWikiPage(resolveProjectId(args, context, true), getString(args, "slug"), {
1734
+ query: toQuery(omit(args, ["project_id", "slug"]))
1735
+ })
1736
+ },
1737
+ {
1738
+ name: "gitlab_create_wiki_page",
1739
+ title: "Create Wiki Page",
1740
+ description: "Create a wiki page.",
1741
+ mutating: true,
1742
+ requiresFeature: "wiki",
1743
+ inputSchema: {
1744
+ project_id: z.string().optional(),
1745
+ title: z.string().min(1),
1746
+ content: z.string().min(1),
1747
+ format: optionalString
1748
+ },
1749
+ handler: async (args, context) =>
1750
+ context.gitlab.createWikiPage(resolveProjectId(args, context, true), {
1751
+ title: getString(args, "title"),
1752
+ content: getString(args, "content"),
1753
+ format: getOptionalString(args, "format") as
1754
+ | "markdown"
1755
+ | "rdoc"
1756
+ | "asciidoc"
1757
+ | "org"
1758
+ | undefined
1759
+ })
1760
+ },
1761
+ {
1762
+ name: "gitlab_update_wiki_page",
1763
+ title: "Update Wiki Page",
1764
+ description: "Update wiki page by slug.",
1765
+ mutating: true,
1766
+ requiresFeature: "wiki",
1767
+ inputSchema: {
1768
+ project_id: z.string().optional(),
1769
+ slug: z.string().min(1),
1770
+ content: z.string().min(1),
1771
+ title: optionalString,
1772
+ format: optionalString
1773
+ },
1774
+ handler: async (args, context) =>
1775
+ context.gitlab.updateWikiPage(
1776
+ resolveProjectId(args, context, true),
1777
+ getString(args, "slug"),
1778
+ {
1779
+ content: getString(args, "content"),
1780
+ title: getOptionalString(args, "title"),
1781
+ format: getOptionalString(args, "format") as
1782
+ | "markdown"
1783
+ | "rdoc"
1784
+ | "asciidoc"
1785
+ | "org"
1786
+ | undefined
1787
+ }
1788
+ )
1789
+ },
1790
+ {
1791
+ name: "gitlab_delete_wiki_page",
1792
+ title: "Delete Wiki Page",
1793
+ description: "Delete wiki page by slug.",
1794
+ mutating: true,
1795
+ requiresFeature: "wiki",
1796
+ inputSchema: {
1797
+ project_id: z.string().optional(),
1798
+ slug: z.string().min(1)
1799
+ },
1800
+ handler: async (args, context) =>
1801
+ context.gitlab.deleteWikiPage(
1802
+ resolveProjectId(args, context, true),
1803
+ getString(args, "slug")
1804
+ )
1805
+ },
1806
+ {
1807
+ name: "gitlab_list_pipelines",
1808
+ title: "List Pipelines",
1809
+ description: "List pipelines for a project.",
1810
+ mutating: false,
1811
+ requiresFeature: "pipeline",
1812
+ inputSchema: {
1813
+ project_id: z.string().optional(),
1814
+ scope: z.enum(["running", "pending", "finished", "branches", "tags"]).optional(),
1815
+ status: z
1816
+ .enum([
1817
+ "created",
1818
+ "waiting_for_resource",
1819
+ "preparing",
1820
+ "pending",
1821
+ "running",
1822
+ "success",
1823
+ "failed",
1824
+ "canceled",
1825
+ "skipped",
1826
+ "manual",
1827
+ "scheduled"
1828
+ ])
1829
+ .optional(),
1830
+ ref: optionalString,
1831
+ sha: optionalString,
1832
+ yaml_errors: optionalBoolean,
1833
+ username: optionalString,
1834
+ updated_after: optionalString,
1835
+ updated_before: optionalString,
1836
+ order_by: z.enum(["id", "status", "ref", "updated_at", "user_id"]).optional(),
1837
+ sort: z.enum(["asc", "desc"]).optional(),
1838
+ source: optionalString,
1839
+ ...paginationShape
1840
+ },
1841
+ handler: async (args, context) =>
1842
+ context.gitlab.listPipelines(resolveProjectId(args, context, true), {
1843
+ query: toQuery(omit(args, ["project_id"]))
1844
+ })
1845
+ },
1846
+ {
1847
+ name: "gitlab_get_pipeline",
1848
+ title: "Get Pipeline",
1849
+ description: "Get one pipeline.",
1850
+ mutating: false,
1851
+ requiresFeature: "pipeline",
1852
+ inputSchema: {
1853
+ project_id: z.string().optional(),
1854
+ pipeline_id: z.string().min(1)
1855
+ },
1856
+ handler: async (args, context) =>
1857
+ context.gitlab.getPipeline(
1858
+ resolveProjectId(args, context, true),
1859
+ getString(args, "pipeline_id")
1860
+ )
1861
+ },
1862
+ {
1863
+ name: "gitlab_list_pipeline_jobs",
1864
+ title: "List Pipeline Jobs",
1865
+ description: "List jobs in a pipeline.",
1866
+ mutating: false,
1867
+ requiresFeature: "pipeline",
1868
+ inputSchema: {
1869
+ project_id: z.string().optional(),
1870
+ pipeline_id: z.string().min(1),
1871
+ scope: z
1872
+ .enum([
1873
+ "created",
1874
+ "pending",
1875
+ "running",
1876
+ "failed",
1877
+ "success",
1878
+ "canceled",
1879
+ "skipped",
1880
+ "manual"
1881
+ ])
1882
+ .optional(),
1883
+ include_retried: optionalBoolean,
1884
+ ...paginationShape
1885
+ },
1886
+ handler: async (args, context) =>
1887
+ context.gitlab.listPipelineJobs(
1888
+ resolveProjectId(args, context, true),
1889
+ getString(args, "pipeline_id"),
1890
+ { query: toQuery(omit(args, ["project_id", "pipeline_id"])) }
1891
+ )
1892
+ },
1893
+ {
1894
+ name: "gitlab_list_pipeline_trigger_jobs",
1895
+ title: "List Pipeline Trigger Jobs",
1896
+ description: "List downstream/bridge trigger jobs in a pipeline.",
1897
+ mutating: false,
1898
+ requiresFeature: "pipeline",
1899
+ inputSchema: {
1900
+ project_id: z.string().optional(),
1901
+ pipeline_id: z.string().min(1),
1902
+ scope: z
1903
+ .enum([
1904
+ "canceled",
1905
+ "canceling",
1906
+ "created",
1907
+ "failed",
1908
+ "manual",
1909
+ "pending",
1910
+ "preparing",
1911
+ "running",
1912
+ "scheduled",
1913
+ "skipped",
1914
+ "success",
1915
+ "waiting_for_resource"
1916
+ ])
1917
+ .optional(),
1918
+ ...paginationShape
1919
+ },
1920
+ handler: async (args, context) =>
1921
+ context.gitlab.listPipelineTriggerJobs(
1922
+ resolveProjectId(args, context, true),
1923
+ getString(args, "pipeline_id"),
1924
+ { query: toQuery(omit(args, ["project_id", "pipeline_id"])) }
1925
+ )
1926
+ },
1927
+ {
1928
+ name: "gitlab_get_pipeline_job",
1929
+ title: "Get Pipeline Job",
1930
+ description: "Get one job by job ID.",
1931
+ mutating: false,
1932
+ requiresFeature: "pipeline",
1933
+ inputSchema: {
1934
+ project_id: z.string().optional(),
1935
+ job_id: z.string().min(1)
1936
+ },
1937
+ handler: async (args, context) =>
1938
+ context.gitlab.getPipelineJob(
1939
+ resolveProjectId(args, context, true),
1940
+ getString(args, "job_id")
1941
+ )
1942
+ },
1943
+ {
1944
+ name: "gitlab_get_pipeline_job_output",
1945
+ title: "Get Pipeline Job Output",
1946
+ description: "Get raw job trace output.",
1947
+ mutating: false,
1948
+ requiresFeature: "pipeline",
1949
+ inputSchema: {
1950
+ project_id: z.string().optional(),
1951
+ job_id: z.string().min(1)
1952
+ },
1953
+ handler: async (args, context) =>
1954
+ context.gitlab.getPipelineJobOutput(
1955
+ resolveProjectId(args, context, true),
1956
+ getString(args, "job_id")
1957
+ )
1958
+ },
1959
+ {
1960
+ name: "gitlab_create_pipeline",
1961
+ title: "Create Pipeline",
1962
+ description: "Trigger a new pipeline.",
1963
+ mutating: true,
1964
+ requiresFeature: "pipeline",
1965
+ inputSchema: {
1966
+ project_id: z.string().optional(),
1967
+ ref: z.string().min(1),
1968
+ variables: z
1969
+ .array(
1970
+ z.object({
1971
+ key: z.string(),
1972
+ value: z.string(),
1973
+ variable_type: optionalString
1974
+ })
1975
+ )
1976
+ .optional()
1977
+ },
1978
+ handler: async (args, context) =>
1979
+ context.gitlab.createPipeline(resolveProjectId(args, context, true), {
1980
+ ref: getString(args, "ref"),
1981
+ variables: getArray(args, "variables") as Array<{
1982
+ key: string;
1983
+ value: string;
1984
+ variable_type?: "env_var" | "file";
1985
+ }>
1986
+ })
1987
+ },
1988
+ {
1989
+ name: "gitlab_retry_pipeline",
1990
+ title: "Retry Pipeline",
1991
+ description: "Retry failed jobs in pipeline.",
1992
+ mutating: true,
1993
+ requiresFeature: "pipeline",
1994
+ inputSchema: {
1995
+ project_id: z.string().optional(),
1996
+ pipeline_id: z.string().min(1)
1997
+ },
1998
+ handler: async (args, context) =>
1999
+ context.gitlab.retryPipeline(
2000
+ resolveProjectId(args, context, true),
2001
+ getString(args, "pipeline_id")
2002
+ )
2003
+ },
2004
+ {
2005
+ name: "gitlab_cancel_pipeline",
2006
+ title: "Cancel Pipeline",
2007
+ description: "Cancel a running pipeline.",
2008
+ mutating: true,
2009
+ requiresFeature: "pipeline",
2010
+ inputSchema: {
2011
+ project_id: z.string().optional(),
2012
+ pipeline_id: z.string().min(1)
2013
+ },
2014
+ handler: async (args, context) =>
2015
+ context.gitlab.cancelPipeline(
2016
+ resolveProjectId(args, context, true),
2017
+ getString(args, "pipeline_id")
2018
+ )
2019
+ },
2020
+ {
2021
+ name: "gitlab_retry_pipeline_job",
2022
+ title: "Retry Pipeline Job",
2023
+ description: "Retry one failed job.",
2024
+ mutating: true,
2025
+ requiresFeature: "pipeline",
2026
+ inputSchema: {
2027
+ project_id: z.string().optional(),
2028
+ job_id: z.string().min(1)
2029
+ },
2030
+ handler: async (args, context) =>
2031
+ context.gitlab.retryPipelineJob(
2032
+ resolveProjectId(args, context, true),
2033
+ getString(args, "job_id")
2034
+ )
2035
+ },
2036
+ {
2037
+ name: "gitlab_cancel_pipeline_job",
2038
+ title: "Cancel Pipeline Job",
2039
+ description: "Cancel one running job.",
2040
+ mutating: true,
2041
+ requiresFeature: "pipeline",
2042
+ inputSchema: {
2043
+ project_id: z.string().optional(),
2044
+ job_id: z.string().min(1)
2045
+ },
2046
+ handler: async (args, context) =>
2047
+ context.gitlab.cancelPipelineJob(
2048
+ resolveProjectId(args, context, true),
2049
+ getString(args, "job_id")
2050
+ )
2051
+ },
2052
+ {
2053
+ name: "gitlab_play_pipeline_job",
2054
+ title: "Play Pipeline Job",
2055
+ description: "Play a manual job.",
2056
+ mutating: true,
2057
+ requiresFeature: "pipeline",
2058
+ inputSchema: {
2059
+ project_id: z.string().optional(),
2060
+ job_id: z.string().min(1)
2061
+ },
2062
+ handler: async (args, context) =>
2063
+ context.gitlab.playPipelineJob(
2064
+ resolveProjectId(args, context, true),
2065
+ getString(args, "job_id")
2066
+ )
2067
+ },
2068
+ {
2069
+ name: "gitlab_list_milestones",
2070
+ title: "List Milestones",
2071
+ description: "List project milestones.",
2072
+ mutating: false,
2073
+ requiresFeature: "milestone",
2074
+ inputSchema: {
2075
+ project_id: z.string().optional(),
2076
+ iids: optionalNumberArray,
2077
+ state: optionalString,
2078
+ title: optionalString,
2079
+ search: optionalString,
2080
+ include_ancestors: optionalBoolean,
2081
+ updated_before: optionalString,
2082
+ updated_after: optionalString,
2083
+ ...paginationShape
2084
+ },
2085
+ handler: async (args, context) =>
2086
+ context.gitlab.listMilestones(resolveProjectId(args, context, true), {
2087
+ query: toQuery(omit(args, ["project_id"]))
2088
+ })
2089
+ },
2090
+ {
2091
+ name: "gitlab_get_milestone",
2092
+ title: "Get Milestone",
2093
+ description: "Get a milestone by ID.",
2094
+ mutating: false,
2095
+ requiresFeature: "milestone",
2096
+ inputSchema: {
2097
+ project_id: z.string().optional(),
2098
+ milestone_id: z.string().min(1)
2099
+ },
2100
+ handler: async (args, context) =>
2101
+ context.gitlab.getMilestone(
2102
+ resolveProjectId(args, context, true),
2103
+ getString(args, "milestone_id")
2104
+ )
2105
+ },
2106
+ {
2107
+ name: "gitlab_create_milestone",
2108
+ title: "Create Milestone",
2109
+ description: "Create a milestone.",
2110
+ mutating: true,
2111
+ requiresFeature: "milestone",
2112
+ inputSchema: {
2113
+ project_id: z.string().optional(),
2114
+ title: z.string().min(1),
2115
+ description: optionalString,
2116
+ due_date: optionalString,
2117
+ start_date: optionalString
2118
+ },
2119
+ handler: async (args, context) =>
2120
+ context.gitlab.createMilestone(resolveProjectId(args, context, true), {
2121
+ title: getString(args, "title"),
2122
+ description: getOptionalString(args, "description"),
2123
+ due_date: getOptionalString(args, "due_date"),
2124
+ start_date: getOptionalString(args, "start_date")
2125
+ })
2126
+ },
2127
+ {
2128
+ name: "gitlab_update_milestone",
2129
+ title: "Update Milestone",
2130
+ description: "Update milestone fields.",
2131
+ mutating: true,
2132
+ requiresFeature: "milestone",
2133
+ inputSchema: {
2134
+ project_id: z.string().optional(),
2135
+ milestone_id: z.string().min(1),
2136
+ title: optionalString,
2137
+ description: optionalString,
2138
+ due_date: optionalString,
2139
+ start_date: optionalString,
2140
+ state_event: optionalString
2141
+ },
2142
+ handler: async (args, context) =>
2143
+ context.gitlab.updateMilestone(
2144
+ resolveProjectId(args, context, true),
2145
+ getString(args, "milestone_id"),
2146
+ toQuery(omit(args, ["project_id", "milestone_id"]))
2147
+ )
2148
+ },
2149
+ {
2150
+ name: "gitlab_edit_milestone",
2151
+ title: "Edit Milestone (Alias)",
2152
+ description: "Backward-compatible alias of gitlab_update_milestone.",
2153
+ mutating: true,
2154
+ requiresFeature: "milestone",
2155
+ inputSchema: {
2156
+ project_id: z.string().optional(),
2157
+ milestone_id: z.string().min(1),
2158
+ title: optionalString,
2159
+ description: optionalString,
2160
+ due_date: optionalString,
2161
+ start_date: optionalString,
2162
+ state_event: optionalString
2163
+ },
2164
+ handler: async (args, context) =>
2165
+ context.gitlab.updateMilestone(
2166
+ resolveProjectId(args, context, true),
2167
+ getString(args, "milestone_id"),
2168
+ toQuery(omit(args, ["project_id", "milestone_id"]))
2169
+ )
2170
+ },
2171
+ {
2172
+ name: "gitlab_delete_milestone",
2173
+ title: "Delete Milestone",
2174
+ description: "Delete a milestone.",
2175
+ mutating: true,
2176
+ requiresFeature: "milestone",
2177
+ inputSchema: {
2178
+ project_id: z.string().optional(),
2179
+ milestone_id: z.string().min(1)
2180
+ },
2181
+ handler: async (args, context) =>
2182
+ context.gitlab.deleteMilestone(
2183
+ resolveProjectId(args, context, true),
2184
+ getString(args, "milestone_id")
2185
+ )
2186
+ },
2187
+ {
2188
+ name: "gitlab_get_milestone_issue",
2189
+ title: "Get Milestone Issues",
2190
+ description: "List issues assigned to a milestone.",
2191
+ mutating: false,
2192
+ requiresFeature: "milestone",
2193
+ inputSchema: {
2194
+ project_id: z.string().optional(),
2195
+ milestone_id: z.string().min(1)
2196
+ },
2197
+ handler: async (args, context) =>
2198
+ context.gitlab.getMilestoneIssues(
2199
+ resolveProjectId(args, context, true),
2200
+ getString(args, "milestone_id")
2201
+ )
2202
+ },
2203
+ {
2204
+ name: "gitlab_get_milestone_merge_requests",
2205
+ title: "Get Milestone Merge Requests",
2206
+ description: "List merge requests assigned to a milestone.",
2207
+ mutating: false,
2208
+ requiresFeature: "milestone",
2209
+ inputSchema: {
2210
+ project_id: z.string().optional(),
2211
+ milestone_id: z.string().min(1),
2212
+ ...paginationShape
2213
+ },
2214
+ handler: async (args, context) =>
2215
+ context.gitlab.getMilestoneMergeRequests(
2216
+ resolveProjectId(args, context, true),
2217
+ getString(args, "milestone_id"),
2218
+ { query: toQuery(omit(args, ["project_id", "milestone_id"])) }
2219
+ )
2220
+ },
2221
+ {
2222
+ name: "gitlab_promote_milestone",
2223
+ title: "Promote Milestone",
2224
+ description: "Promote a project milestone to a group milestone.",
2225
+ mutating: true,
2226
+ requiresFeature: "milestone",
2227
+ inputSchema: {
2228
+ project_id: z.string().optional(),
2229
+ milestone_id: z.string().min(1)
2230
+ },
2231
+ handler: async (args, context) =>
2232
+ context.gitlab.promoteMilestone(
2233
+ resolveProjectId(args, context, true),
2234
+ getString(args, "milestone_id")
2235
+ )
2236
+ },
2237
+ {
2238
+ name: "gitlab_get_milestone_burndown_events",
2239
+ title: "Get Milestone Burndown Events",
2240
+ description: "List burndown events for a milestone.",
2241
+ mutating: false,
2242
+ requiresFeature: "milestone",
2243
+ inputSchema: {
2244
+ project_id: z.string().optional(),
2245
+ milestone_id: z.string().min(1),
2246
+ ...paginationShape
2247
+ },
2248
+ handler: async (args, context) =>
2249
+ context.gitlab.getMilestoneBurndownEvents(
2250
+ resolveProjectId(args, context, true),
2251
+ getString(args, "milestone_id"),
2252
+ { query: toQuery(omit(args, ["project_id", "milestone_id"])) }
2253
+ )
2254
+ },
2255
+ {
2256
+ name: "gitlab_list_releases",
2257
+ title: "List Releases",
2258
+ description: "List project releases.",
2259
+ mutating: false,
2260
+ requiresFeature: "release",
2261
+ inputSchema: {
2262
+ project_id: z.string().optional(),
2263
+ order_by: z.enum(["released_at", "created_at"]).optional(),
2264
+ sort: z.enum(["asc", "desc"]).optional(),
2265
+ include_html_description: optionalBoolean,
2266
+ ...paginationShape
2267
+ },
2268
+ handler: async (args, context) =>
2269
+ context.gitlab.listReleases(resolveProjectId(args, context, true), {
2270
+ query: toQuery(omit(args, ["project_id"]))
2271
+ })
2272
+ },
2273
+ {
2274
+ name: "gitlab_get_release",
2275
+ title: "Get Release",
2276
+ description: "Get one release by tag name.",
2277
+ mutating: false,
2278
+ requiresFeature: "release",
2279
+ inputSchema: {
2280
+ project_id: z.string().optional(),
2281
+ tag_name: z.string().min(1),
2282
+ include_html_description: optionalBoolean
2283
+ },
2284
+ handler: async (args, context) =>
2285
+ context.gitlab.getRelease(
2286
+ resolveProjectId(args, context, true),
2287
+ getString(args, "tag_name"),
2288
+ { query: toQuery(omit(args, ["project_id", "tag_name"])) }
2289
+ )
2290
+ },
2291
+ {
2292
+ name: "gitlab_create_release",
2293
+ title: "Create Release",
2294
+ description: "Create a release.",
2295
+ mutating: true,
2296
+ requiresFeature: "release",
2297
+ inputSchema: {
2298
+ project_id: z.string().optional(),
2299
+ name: optionalString,
2300
+ tag_name: z.string().min(1),
2301
+ tag_message: optionalString,
2302
+ description: optionalString,
2303
+ ref: optionalString,
2304
+ released_at: optionalString,
2305
+ milestones: optionalStringArray,
2306
+ assets: optionalRecord
2307
+ },
2308
+ handler: async (args, context) =>
2309
+ context.gitlab.createRelease(
2310
+ resolveProjectId(args, context, true),
2311
+ toQuery(omit(args, ["project_id"]))
2312
+ )
2313
+ },
2314
+ {
2315
+ name: "gitlab_update_release",
2316
+ title: "Update Release",
2317
+ description: "Update existing release.",
2318
+ mutating: true,
2319
+ requiresFeature: "release",
2320
+ inputSchema: {
2321
+ project_id: z.string().optional(),
2322
+ tag_name: z.string().min(1),
2323
+ name: optionalString,
2324
+ description: optionalString,
2325
+ released_at: optionalString,
2326
+ milestones: optionalStringArray,
2327
+ assets: optionalRecord
2328
+ },
2329
+ handler: async (args, context) =>
2330
+ context.gitlab.updateRelease(
2331
+ resolveProjectId(args, context, true),
2332
+ getString(args, "tag_name"),
2333
+ toQuery(omit(args, ["project_id", "tag_name"]))
2334
+ )
2335
+ },
2336
+ {
2337
+ name: "gitlab_delete_release",
2338
+ title: "Delete Release",
2339
+ description: "Delete a release by tag.",
2340
+ mutating: true,
2341
+ requiresFeature: "release",
2342
+ inputSchema: {
2343
+ project_id: z.string().optional(),
2344
+ tag_name: z.string().min(1)
2345
+ },
2346
+ handler: async (args, context) =>
2347
+ context.gitlab.deleteRelease(
2348
+ resolveProjectId(args, context, true),
2349
+ getString(args, "tag_name")
2350
+ )
2351
+ },
2352
+ {
2353
+ name: "gitlab_create_release_evidence",
2354
+ title: "Create Release Evidence",
2355
+ description: "Create evidence for an existing release.",
2356
+ mutating: true,
2357
+ requiresFeature: "release",
2358
+ inputSchema: {
2359
+ project_id: z.string().optional(),
2360
+ tag_name: z.string().min(1)
2361
+ },
2362
+ handler: async (args, context) =>
2363
+ context.gitlab.createReleaseEvidence(
2364
+ resolveProjectId(args, context, true),
2365
+ getString(args, "tag_name")
2366
+ )
2367
+ },
2368
+ {
2369
+ name: "gitlab_download_release_asset",
2370
+ title: "Download Release Asset",
2371
+ description: "Download a release asset using its direct asset path.",
2372
+ mutating: false,
2373
+ requiresFeature: "release",
2374
+ inputSchema: {
2375
+ project_id: z.string().optional(),
2376
+ tag_name: z.string().min(1),
2377
+ direct_asset_path: z.string().min(1)
2378
+ },
2379
+ handler: async (args, context) =>
2380
+ context.gitlab.downloadReleaseAsset(
2381
+ resolveProjectId(args, context, true),
2382
+ getString(args, "tag_name"),
2383
+ getString(args, "direct_asset_path")
2384
+ )
2385
+ },
2386
+ {
2387
+ name: "gitlab_list_labels",
2388
+ title: "List Labels",
2389
+ description: "List project labels.",
2390
+ mutating: false,
2391
+ inputSchema: {
2392
+ project_id: z.string().optional(),
2393
+ with_counts: optionalBoolean,
2394
+ include_ancestor_groups: optionalBoolean,
2395
+ search: optionalString,
2396
+ ...paginationShape
2397
+ },
2398
+ handler: async (args, context) =>
2399
+ context.gitlab.listLabels(resolveProjectId(args, context, true), {
2400
+ query: toQuery(omit(args, ["project_id"]))
2401
+ })
2402
+ },
2403
+ {
2404
+ name: "gitlab_get_label",
2405
+ title: "Get Label",
2406
+ description: "Get one label by ID.",
2407
+ mutating: false,
2408
+ inputSchema: {
2409
+ project_id: z.string().optional(),
2410
+ label_id: z.string().min(1),
2411
+ include_ancestor_groups: optionalBoolean
2412
+ },
2413
+ handler: async (args, context) =>
2414
+ context.gitlab.getLabel(
2415
+ resolveProjectId(args, context, true),
2416
+ getString(args, "label_id"),
2417
+ {
2418
+ query: toQuery(omit(args, ["project_id", "label_id"]))
2419
+ }
2420
+ )
2421
+ },
2422
+ {
2423
+ name: "gitlab_create_label",
2424
+ title: "Create Label",
2425
+ description: "Create a label.",
2426
+ mutating: true,
2427
+ inputSchema: {
2428
+ project_id: z.string().optional(),
2429
+ name: z.string().min(1),
2430
+ color: z.string().min(1),
2431
+ description: optionalString,
2432
+ priority: optionalNumber
2433
+ },
2434
+ handler: async (args, context) =>
2435
+ context.gitlab.createLabel(
2436
+ resolveProjectId(args, context, true),
2437
+ toQuery(omit(args, ["project_id"]))
2438
+ )
2439
+ },
2440
+ {
2441
+ name: "gitlab_update_label",
2442
+ title: "Update Label",
2443
+ description: "Update a label.",
2444
+ mutating: true,
2445
+ inputSchema: {
2446
+ project_id: z.string().optional(),
2447
+ name: optionalString,
2448
+ label_id: optionalString,
2449
+ new_name: optionalString,
2450
+ color: optionalString,
2451
+ description: optionalString,
2452
+ priority: optionalNumber
2453
+ },
2454
+ handler: async (args, context) => {
2455
+ const payload = toQuery(omit(args, ["project_id"])) as Record<string, unknown>;
2456
+ if (payload.name === undefined) {
2457
+ payload.name = getOptionalString(args, "label_id");
2458
+ }
2459
+ if (payload.name === undefined) {
2460
+ throw new Error("Either name or label_id must be provided");
2461
+ }
2462
+
2463
+ return context.gitlab.updateLabel(resolveProjectId(args, context, true), payload);
2464
+ }
2465
+ },
2466
+ {
2467
+ name: "gitlab_delete_label",
2468
+ title: "Delete Label",
2469
+ description: "Delete a label by name.",
2470
+ mutating: true,
2471
+ inputSchema: {
2472
+ project_id: z.string().optional(),
2473
+ name: optionalString,
2474
+ label_id: optionalString
2475
+ },
2476
+ handler: async (args, context) => {
2477
+ const labelName = getOptionalString(args, "name") ?? getOptionalString(args, "label_id");
2478
+ if (!labelName) {
2479
+ throw new Error("Either name or label_id must be provided");
2480
+ }
2481
+ return context.gitlab.deleteLabel(resolveProjectId(args, context, true), labelName);
2482
+ }
2483
+ },
2484
+ {
2485
+ name: "gitlab_list_namespaces",
2486
+ title: "List Namespaces",
2487
+ description: "List namespaces visible to user.",
2488
+ mutating: false,
2489
+ inputSchema: {
2490
+ search: optionalString,
2491
+ owned: optionalBoolean,
2492
+ ...paginationShape
2493
+ },
2494
+ handler: async (args, context) => context.gitlab.listNamespaces({ query: toQuery(args) })
2495
+ },
2496
+ {
2497
+ name: "gitlab_get_namespace",
2498
+ title: "Get Namespace",
2499
+ description: "Get namespace by ID or path.",
2500
+ mutating: false,
2501
+ inputSchema: {
2502
+ namespace_id_or_path: optionalString,
2503
+ namespace_id: optionalString
2504
+ },
2505
+ handler: async (args, context) => {
2506
+ const namespaceId =
2507
+ getOptionalString(args, "namespace_id_or_path") ??
2508
+ getOptionalString(args, "namespace_id");
2509
+ if (!namespaceId) {
2510
+ throw new Error("Either namespace_id_or_path or namespace_id must be provided");
2511
+ }
2512
+
2513
+ return context.gitlab.getNamespace(namespaceId);
2514
+ }
2515
+ },
2516
+ {
2517
+ name: "gitlab_verify_namespace",
2518
+ title: "Verify Namespace",
2519
+ description: "Verify if namespace path exists.",
2520
+ mutating: false,
2521
+ inputSchema: {
2522
+ path: z.string().min(1)
2523
+ },
2524
+ handler: async (args, context) => context.gitlab.verifyNamespace(getString(args, "path"))
2525
+ },
2526
+ {
2527
+ name: "gitlab_get_users",
2528
+ title: "Get Users",
2529
+ description: "Search users.",
2530
+ mutating: false,
2531
+ inputSchema: {
2532
+ username: optionalString,
2533
+ search: optionalString,
2534
+ active: optionalBoolean,
2535
+ extern_uid: optionalString,
2536
+ provider: optionalString,
2537
+ ...paginationShape
2538
+ },
2539
+ handler: async (args, context) => context.gitlab.getUsers({ query: toQuery(args) })
2540
+ },
2541
+ {
2542
+ name: "gitlab_list_events",
2543
+ title: "List Events",
2544
+ description: "List current user events.",
2545
+ mutating: false,
2546
+ inputSchema: {
2547
+ action: optionalString,
2548
+ target_type: optionalString,
2549
+ before: optionalString,
2550
+ after: optionalString,
2551
+ scope: optionalString,
2552
+ sort: optionalString,
2553
+ ...paginationShape
2554
+ },
2555
+ handler: async (args, context) => context.gitlab.listEvents({ query: toQuery(args) })
2556
+ },
2557
+ {
2558
+ name: "gitlab_get_project_events",
2559
+ title: "Get Project Events",
2560
+ description: "List events for a specific project.",
2561
+ mutating: false,
2562
+ inputSchema: {
2563
+ project_id: z.string().optional(),
2564
+ action: optionalString,
2565
+ target_type: optionalString,
2566
+ before: optionalString,
2567
+ after: optionalString,
2568
+ sort: optionalString,
2569
+ ...paginationShape
2570
+ },
2571
+ handler: async (args, context) =>
2572
+ context.gitlab.getProjectEvents(resolveProjectId(args, context, true), {
2573
+ query: toQuery(omit(args, ["project_id"]))
2574
+ })
2575
+ },
2576
+ {
2577
+ name: "gitlab_upload_markdown",
2578
+ title: "Upload Markdown",
2579
+ description: "Upload markdown file/attachment to project.",
2580
+ mutating: true,
2581
+ inputSchema: {
2582
+ project_id: z.string().optional(),
2583
+ content: optionalString,
2584
+ filename: z.string().default("upload.md"),
2585
+ file_path: optionalString
2586
+ },
2587
+ handler: async (args, context) => {
2588
+ const projectId = resolveProjectId(args, context, true);
2589
+ const filePath = getOptionalString(args, "file_path");
2590
+ if (filePath) {
2591
+ return context.gitlab.uploadMarkdownFile(projectId, filePath);
2592
+ }
2593
+
2594
+ const content = getOptionalString(args, "content");
2595
+ if (!content) {
2596
+ throw new Error("Either file_path or content must be provided");
2597
+ }
2598
+
2599
+ return context.gitlab.uploadMarkdown(projectId, content, getString(args, "filename"));
2600
+ }
2601
+ },
2602
+ {
2603
+ name: "gitlab_download_attachment",
2604
+ title: "Download Attachment",
2605
+ description: "Download attachment by URL/path and return base64.",
2606
+ mutating: false,
2607
+ inputSchema: {
2608
+ project_id: z.string().optional(),
2609
+ url_or_path: optionalString,
2610
+ secret: optionalString,
2611
+ filename: optionalString,
2612
+ local_path: optionalString
2613
+ },
2614
+ handler: async (args, context) => {
2615
+ const urlOrPath = getOptionalString(args, "url_or_path");
2616
+ if (urlOrPath) {
2617
+ const projectId = resolveProjectId(args, context, false);
2618
+ const upload = parseProjectUploadReference(urlOrPath);
2619
+
2620
+ if (context.env.GITLAB_ALLOWED_PROJECT_IDS.length > 0) {
2621
+ if (!projectId) {
2622
+ throw new Error(
2623
+ "project_id is required when GITLAB_ALLOWED_PROJECT_IDS is configured"
2624
+ );
2625
+ }
2626
+
2627
+ if (!upload) {
2628
+ throw new Error(
2629
+ "In project-scoped mode, url_or_path must be a GitLab upload URL/path like '/uploads/<secret>/<filename>'"
2630
+ );
2631
+ }
2632
+
2633
+ const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(upload.secret)}/${encodeURIComponent(upload.filename)}`;
2634
+ return context.gitlab.downloadAttachment(apiRelativePath);
2635
+ }
2636
+
2637
+ if (projectId && upload) {
2638
+ const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(upload.secret)}/${encodeURIComponent(upload.filename)}`;
2639
+ return context.gitlab.downloadAttachment(apiRelativePath);
2640
+ }
2641
+
2642
+ return context.gitlab.downloadAttachment(urlOrPath);
2643
+ }
2644
+
2645
+ const secret = getOptionalString(args, "secret");
2646
+ const filename = getOptionalString(args, "filename");
2647
+ if (!secret || !filename) {
2648
+ throw new Error(
2649
+ "Either url_or_path must be provided, or both secret and filename must be provided"
2650
+ );
2651
+ }
2652
+
2653
+ const projectId = resolveProjectId(args, context, true);
2654
+ const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(secret)}/${encodeURIComponent(filename)}`;
2655
+
2656
+ return context.gitlab.downloadAttachment(apiRelativePath);
2657
+ }
2658
+ },
2659
+ {
2660
+ name: "gitlab_execute_graphql_query",
2661
+ title: "Execute GraphQL Query",
2662
+ description: "Execute read-only GraphQL query.",
2663
+ mutating: false,
2664
+ inputSchema: {
2665
+ query: z.string().min(1),
2666
+ variables: optionalRecord
2667
+ },
2668
+ handler: async (args, context) => {
2669
+ const query = getString(args, "query");
2670
+
2671
+ if (containsGraphqlMutation(query)) {
2672
+ throw new Error(
2673
+ "Mutation detected. Use gitlab_execute_graphql_mutation for mutation operations."
2674
+ );
2675
+ }
2676
+
2677
+ return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
2678
+ }
2679
+ },
2680
+ {
2681
+ name: "gitlab_execute_graphql_mutation",
2682
+ title: "Execute GraphQL Mutation",
2683
+ description: "Execute GraphQL mutation (disabled in read-only mode).",
2684
+ mutating: true,
2685
+ inputSchema: {
2686
+ query: z.string().min(1),
2687
+ variables: optionalRecord
2688
+ },
2689
+ handler: async (args, context) => {
2690
+ const query = getString(args, "query");
2691
+
2692
+ if (!containsGraphqlMutation(query)) {
2693
+ throw new Error("No mutation detected. Use gitlab_execute_graphql_query for queries.");
2694
+ }
2695
+
2696
+ return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
2697
+ }
2698
+ },
2699
+ {
2700
+ name: "gitlab_execute_graphql",
2701
+ title: "Execute GraphQL (Compat)",
2702
+ description:
2703
+ "Backward-compatible GraphQL executor. Mutation payloads still honor read-only policy.",
2704
+ mutating: false,
2705
+ inputSchema: {
2706
+ query: z.string().min(1),
2707
+ variables: optionalRecord
2708
+ },
2709
+ handler: async (args, context) => {
2710
+ const query = getString(args, "query");
2711
+ if (containsGraphqlMutation(query)) {
2712
+ context.policy.assertCanExecute({
2713
+ name: "gitlab_execute_graphql",
2714
+ mutating: true
2715
+ });
2716
+ }
2717
+
2718
+ return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
2719
+ }
2720
+ }
2721
+ ];
2722
+ }
2723
+
2724
+ function assertAuthReady(context: AppContext): void {
2725
+ const auth = getSessionAuth();
2726
+
2727
+ if (context.env.REMOTE_AUTHORIZATION) {
2728
+ const token = auth?.token;
2729
+ if (!token) {
2730
+ throw new Error("Missing remote authorization token for this session");
2731
+ }
2732
+
2733
+ if (context.env.ENABLE_DYNAMIC_API_URL && !auth?.apiUrl) {
2734
+ throw new Error("Missing remote API URL for this session");
2735
+ }
2736
+
2737
+ return;
2738
+ }
2739
+
2740
+ const hasFallbackAuth =
2741
+ Boolean(context.env.GITLAB_PERSONAL_ACCESS_TOKEN) ||
2742
+ Boolean(context.env.GITLAB_USE_OAUTH && context.env.GITLAB_OAUTH_CLIENT_ID) ||
2743
+ Boolean(context.env.GITLAB_TOKEN_SCRIPT) ||
2744
+ Boolean(context.env.GITLAB_TOKEN_FILE) ||
2745
+ Boolean(context.env.GITLAB_AUTH_COOKIE_PATH);
2746
+
2747
+ if (!hasFallbackAuth) {
2748
+ throw new Error(
2749
+ "Authentication required: set GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_TOKEN_SCRIPT, GITLAB_TOKEN_FILE, or GITLAB_AUTH_COOKIE_PATH"
2750
+ );
2751
+ }
2752
+ }
2753
+
2754
+ export function containsGraphqlMutation(query: string): boolean {
2755
+ if (!query.trim()) {
2756
+ return false;
2757
+ }
2758
+
2759
+ // Remove comments and string values to avoid false positives from text content.
2760
+ const normalized = query
2761
+ .replace(/#[^\n]*/g, " ")
2762
+ .replace(/"""[\s\S]*?"""/g, " ")
2763
+ .replace(/"(?:\\.|[^"\\])*"/g, " ");
2764
+
2765
+ return /\bmutation\b\s*(?:[A-Za-z_][A-Za-z0-9_]*)?\s*(?:\(|\{)/i.test(normalized);
2766
+ }
2767
+
2768
+ export function parseProjectUploadReference(
2769
+ input: string
2770
+ ): { secret: string; filename: string } | undefined {
2771
+ const trimmed = input.trim();
2772
+ if (!trimmed) {
2773
+ return undefined;
2774
+ }
2775
+
2776
+ let pathValue = trimmed;
2777
+
2778
+ if (/^https?:\/\//i.test(trimmed)) {
2779
+ try {
2780
+ pathValue = new URL(trimmed).pathname;
2781
+ } catch {
2782
+ return undefined;
2783
+ }
2784
+ }
2785
+
2786
+ const [pathOnly] = pathValue.split(/[?#]/, 1);
2787
+ if (!pathOnly) {
2788
+ return undefined;
2789
+ }
2790
+
2791
+ const marker = "/uploads/";
2792
+ const markerIndex = pathOnly.lastIndexOf(marker);
2793
+ if (markerIndex < 0) {
2794
+ return undefined;
2795
+ }
2796
+
2797
+ const suffix = pathOnly.slice(markerIndex + marker.length);
2798
+ const [secret, ...filenameParts] = suffix.split("/").filter((segment) => segment.length > 0);
2799
+
2800
+ if (!secret || filenameParts.length === 0) {
2801
+ return undefined;
2802
+ }
2803
+
2804
+ const filename = decodeURIComponent(filenameParts.join("/"));
2805
+ if (!filename) {
2806
+ return undefined;
2807
+ }
2808
+
2809
+ return { secret, filename };
2810
+ }
2811
+
2812
+ export function shouldDisableGraphqlTools(
2813
+ allowedProjectIds: string[],
2814
+ allowGraphqlWithProjectScope: boolean
2815
+ ): boolean {
2816
+ return allowedProjectIds.length > 0 && !allowGraphqlWithProjectScope;
2817
+ }
2818
+
2819
+ function isGraphqlToolName(name: string): boolean {
2820
+ return (
2821
+ name === "gitlab_execute_graphql_query" ||
2822
+ name === "gitlab_execute_graphql_mutation" ||
2823
+ name === "gitlab_execute_graphql"
2824
+ );
2825
+ }
2826
+
2827
+ function resolveProjectId(args: ToolArgs, context: AppContext, required: boolean): string {
2828
+ const fromArgs = getOptionalString(args, "project_id");
2829
+ const allowed = context.env.GITLAB_ALLOWED_PROJECT_IDS;
2830
+
2831
+ if (allowed.length > 0) {
2832
+ if (fromArgs && !allowed.includes(fromArgs)) {
2833
+ throw new Error(
2834
+ `Project '${fromArgs}' is not in GITLAB_ALLOWED_PROJECT_IDS: ${allowed.join(", ")}`
2835
+ );
2836
+ }
2837
+
2838
+ if (!fromArgs && allowed.length === 1) {
2839
+ return requireArrayValue(allowed, 0, "GITLAB_ALLOWED_PROJECT_IDS is empty");
2840
+ }
2841
+
2842
+ if (!fromArgs && allowed.length > 1) {
2843
+ throw new Error(
2844
+ `Multiple allowed projects configured (${allowed.join(", ")}). Please specify project_id.`
2845
+ );
2846
+ }
2847
+
2848
+ return fromArgs ?? requireArrayValue(allowed, 0, "GITLAB_ALLOWED_PROJECT_IDS is empty");
2849
+ }
2850
+
2851
+ if (required && !fromArgs) {
2852
+ throw new Error("project_id is required");
2853
+ }
2854
+
2855
+ return fromArgs ?? "";
2856
+ }
2857
+
2858
+ function toToolError(error: unknown, context?: AppContext): CallToolResult {
2859
+ const detailMode = context?.env.GITLAB_ERROR_DETAIL_MODE ?? "full";
2860
+
2861
+ if (error instanceof GitLabApiError) {
2862
+ const payload: Record<string, unknown> = {
2863
+ error: `GitLab API error ${error.status}`
2864
+ };
2865
+ if (detailMode === "full") {
2866
+ payload.details = redactSensitive(error.details);
2867
+ }
2868
+
2869
+ return {
2870
+ isError: true,
2871
+ content: [
2872
+ {
2873
+ type: "text",
2874
+ text: JSON.stringify(payload, null, 2)
2875
+ }
2876
+ ]
2877
+ };
2878
+ }
2879
+
2880
+ if (error instanceof Error) {
2881
+ const message = detailMode === "full" ? error.message : "Request failed";
2882
+ return {
2883
+ isError: true,
2884
+ content: [
2885
+ {
2886
+ type: "text",
2887
+ text: message
2888
+ }
2889
+ ]
2890
+ };
2891
+ }
2892
+
2893
+ return {
2894
+ isError: true,
2895
+ content: [
2896
+ {
2897
+ type: "text",
2898
+ text: "Unknown error"
2899
+ }
2900
+ ]
2901
+ };
2902
+ }
2903
+
2904
+ function toStructuredContent(value: unknown): Record<string, unknown> {
2905
+ if (typeof value === "object" && value !== null) {
2906
+ if (Array.isArray(value)) {
2907
+ return {
2908
+ items: value,
2909
+ count: value.length
2910
+ };
2911
+ }
2912
+
2913
+ return value as Record<string, unknown>;
2914
+ }
2915
+
2916
+ return {
2917
+ value
2918
+ };
2919
+ }
2920
+
2921
+ function omit(args: ToolArgs, keys: string[]): ToolArgs {
2922
+ const result: ToolArgs = {};
2923
+ for (const [key, value] of Object.entries(args)) {
2924
+ if (!keys.includes(key)) {
2925
+ result[key] = value;
2926
+ }
2927
+ }
2928
+
2929
+ return result;
2930
+ }
2931
+
2932
+ function toQuery(args: ToolArgs): Record<string, string | number | boolean | undefined> {
2933
+ const output: Record<string, string | number | boolean | undefined> = {};
2934
+
2935
+ for (const [key, value] of Object.entries(args)) {
2936
+ if (value === undefined || value === null) {
2937
+ continue;
2938
+ }
2939
+
2940
+ if (Array.isArray(value)) {
2941
+ output[key] = value.join(",");
2942
+ continue;
2943
+ }
2944
+
2945
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2946
+ output[key] = value;
2947
+ }
2948
+ }
2949
+
2950
+ return output;
2951
+ }
2952
+
2953
+ function toCsvValue(value: unknown): string | undefined {
2954
+ if (value === undefined || value === null) {
2955
+ return undefined;
2956
+ }
2957
+
2958
+ if (typeof value === "string") {
2959
+ return value;
2960
+ }
2961
+
2962
+ if (Array.isArray(value)) {
2963
+ const items = value.filter((item): item is string => typeof item === "string");
2964
+ return items.length > 0 ? items.join(",") : undefined;
2965
+ }
2966
+
2967
+ return undefined;
2968
+ }
2969
+
2970
+ function pickFirstMergeRequest(value: unknown): Record<string, unknown> | undefined {
2971
+ if (!Array.isArray(value)) {
2972
+ return undefined;
2973
+ }
2974
+
2975
+ const first = value[0];
2976
+ if (typeof first !== "object" || first === null) {
2977
+ return undefined;
2978
+ }
2979
+
2980
+ return first as Record<string, unknown>;
2981
+ }
2982
+
2983
+ function getString(args: ToolArgs, key: string): string {
2984
+ const value = args[key];
2985
+ if (typeof value !== "string" || value.length === 0) {
2986
+ throw new Error(`'${key}' must be a non-empty string`);
2987
+ }
2988
+
2989
+ return value;
2990
+ }
2991
+
2992
+ function getOptionalString(args: ToolArgs, key: string): string | undefined {
2993
+ const value = args[key];
2994
+ if (value === undefined) {
2995
+ return undefined;
2996
+ }
2997
+
2998
+ if (typeof value !== "string") {
2999
+ throw new Error(`'${key}' must be a string`);
3000
+ }
3001
+
3002
+ return value;
3003
+ }
3004
+
3005
+ function getOptionalStringArray(args: ToolArgs, key: string): string[] | undefined {
3006
+ const value = args[key];
3007
+ if (value === undefined) {
3008
+ return undefined;
3009
+ }
3010
+
3011
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
3012
+ throw new Error(`'${key}' must be string[]`);
3013
+ }
3014
+
3015
+ return value;
3016
+ }
3017
+
3018
+ function getBoolean(args: ToolArgs, key: string): boolean {
3019
+ const value = args[key];
3020
+ if (typeof value !== "boolean") {
3021
+ throw new Error(`'${key}' must be boolean`);
3022
+ }
3023
+
3024
+ return value;
3025
+ }
3026
+
3027
+ function getOptionalBoolean(args: ToolArgs, key: string): boolean | undefined {
3028
+ const value = args[key];
3029
+ if (value === undefined) {
3030
+ return undefined;
3031
+ }
3032
+
3033
+ if (typeof value !== "boolean") {
3034
+ throw new Error(`'${key}' must be boolean`);
3035
+ }
3036
+
3037
+ return value;
3038
+ }
3039
+
3040
+ function getOptionalNumber(args: ToolArgs, key: string): number | undefined {
3041
+ const value = args[key];
3042
+ if (value === undefined) {
3043
+ return undefined;
3044
+ }
3045
+
3046
+ if (typeof value !== "number" || Number.isNaN(value)) {
3047
+ throw new Error(`'${key}' must be number`);
3048
+ }
3049
+
3050
+ return value;
3051
+ }
3052
+
3053
+ function getArray(args: ToolArgs, key: string): unknown[] {
3054
+ const value = args[key];
3055
+ if (!Array.isArray(value)) {
3056
+ throw new Error(`'${key}' must be array`);
3057
+ }
3058
+
3059
+ return value;
3060
+ }
3061
+
3062
+ function getOptionalNumberArray(args: ToolArgs, key: string): number[] | undefined {
3063
+ const value = args[key];
3064
+ if (value === undefined) {
3065
+ return undefined;
3066
+ }
3067
+
3068
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "number")) {
3069
+ throw new Error(`'${key}' must be number[]`);
3070
+ }
3071
+
3072
+ return value;
3073
+ }
3074
+
3075
+ function getOptionalRecord(args: ToolArgs, key: string): Record<string, unknown> | undefined {
3076
+ const value = args[key];
3077
+ if (value === undefined) {
3078
+ return undefined;
3079
+ }
3080
+
3081
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3082
+ throw new Error(`'${key}' must be an object`);
3083
+ }
3084
+
3085
+ return value as Record<string, unknown>;
3086
+ }
3087
+
3088
+ function requireArrayValue<T>(items: T[], index: number, errorMessage: string): T {
3089
+ const value = items[index];
3090
+ if (value === undefined) {
3091
+ throw new Error(errorMessage);
3092
+ }
3093
+
3094
+ return value;
3095
+ }
3096
+
3097
+ function redactSensitive(value: unknown): unknown {
3098
+ if (typeof value === "string") {
3099
+ return value
3100
+ .replace(
3101
+ /\b(glpat-[a-z0-9_-]{10,}|ghp_[a-z0-9]{20,}|eyJ[a-zA-Z0-9._-]{20,})\b/g,
3102
+ "[REDACTED]"
3103
+ )
3104
+ .replace(
3105
+ /(private[-_]?token|authorization)["']?\s*[:=]\s*["']?[^"'\s,}]+/gi,
3106
+ "$1=[REDACTED]"
3107
+ );
3108
+ }
3109
+
3110
+ if (Array.isArray(value)) {
3111
+ return value.map((item) => redactSensitive(item));
3112
+ }
3113
+
3114
+ if (value && typeof value === "object") {
3115
+ const input = value as Record<string, unknown>;
3116
+ const output: Record<string, unknown> = {};
3117
+ for (const [key, item] of Object.entries(input)) {
3118
+ if (/token|authorization|password|secret/i.test(key)) {
3119
+ output[key] = "[REDACTED]";
3120
+ continue;
3121
+ }
3122
+ output[key] = redactSensitive(item);
3123
+ }
3124
+ return output;
3125
+ }
3126
+
3127
+ return value;
3128
+ }