gitlab-mcp 1.4.0 → 1.5.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 (39) hide show
  1. package/README.md +45 -34
  2. package/dist/config/env.d.ts +7 -1
  3. package/dist/config/env.js +17 -0
  4. package/dist/config/env.js.map +1 -1
  5. package/dist/http-app.js +422 -17
  6. package/dist/http-app.js.map +1 -1
  7. package/dist/http.js +10 -1
  8. package/dist/http.js.map +1 -1
  9. package/dist/index.js +8 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/download-token.d.ts +24 -0
  12. package/dist/lib/download-token.js +84 -0
  13. package/dist/lib/download-token.js.map +1 -0
  14. package/dist/lib/gitlab-client.d.ts +78 -1
  15. package/dist/lib/gitlab-client.js +451 -7
  16. package/dist/lib/gitlab-client.js.map +1 -1
  17. package/dist/lib/http-auth-guard.d.ts +8 -0
  18. package/dist/lib/http-auth-guard.js +19 -0
  19. package/dist/lib/http-auth-guard.js.map +1 -0
  20. package/dist/lib/mcp-oauth-provider.d.ts +2 -0
  21. package/dist/lib/mcp-oauth-provider.js +61 -0
  22. package/dist/lib/mcp-oauth-provider.js.map +1 -0
  23. package/dist/lib/oauth.d.ts +3 -1
  24. package/dist/lib/oauth.js +5 -5
  25. package/dist/lib/oauth.js.map +1 -1
  26. package/dist/lib/patch-helper.d.ts +13 -0
  27. package/dist/lib/patch-helper.js +156 -0
  28. package/dist/lib/patch-helper.js.map +1 -0
  29. package/dist/lib/request-runtime.d.ts +1 -0
  30. package/dist/lib/request-runtime.js +42 -4
  31. package/dist/lib/request-runtime.js.map +1 -1
  32. package/dist/lib/tool-schema.js +1 -1
  33. package/dist/lib/tool-schema.js.map +1 -1
  34. package/dist/tools/gitlab.js +2690 -52
  35. package/dist/tools/gitlab.js.map +1 -1
  36. package/docs/authentication.md +37 -8
  37. package/docs/configuration.md +7 -4
  38. package/docs/tools.md +153 -51
  39. package/package.json +1 -1
@@ -1,8 +1,11 @@
1
+ import { Buffer } from "node:buffer";
1
2
  import { Kind, parse } from "graphql";
2
3
  import { z } from "zod";
3
4
  import { GitLabApiError } from "../lib/gitlab-client.js";
5
+ import { applySearchReplace, applyUnifiedDiff, parseSearchReplaceBlocks } from "../lib/patch-helper.js";
4
6
  import { bodySchema, displayNameSchema, nullableOptional, optionalBodySchema, optionalDisplayNameSchema, optionalProjectIdSchema, optionalRefLikeSchema, optionalUrlOrPathSchema, projectIdSchema, refLikeSchema, slugSchema } from "../lib/tool-schema.js";
5
7
  import { getSessionAuth } from "../lib/auth-context.js";
8
+ import { createDownloadToken } from "../lib/download-token.js";
6
9
  import { stripNullsDeep } from "../lib/sanitize.js";
7
10
  import { getMergeRequestCodeContext, mergeRequestCodeContextSchema } from "./mr-code-context.js";
8
11
  const readCapabilities = ["read"];
@@ -30,6 +33,40 @@ const paginationShape = {
30
33
  page: optionalNumber,
31
34
  per_page: optionalNumber
32
35
  };
36
+ const emojiNameSchema = z.string().min(1);
37
+ const awardEmojiIdSchema = z.string().min(1);
38
+ const workItemTypes = [
39
+ "issue",
40
+ "task",
41
+ "incident",
42
+ "test_case",
43
+ "epic",
44
+ "key_result",
45
+ "objective",
46
+ "requirement",
47
+ "ticket"
48
+ ];
49
+ const workItemTypeSchema = z.preprocess((value) => (typeof value === "string" ? value.toLowerCase() : value), z.enum(workItemTypes));
50
+ const optionalWorkItemType = nullableOptional(workItemTypeSchema);
51
+ const workItemIidSchema = z.coerce.number().int().positive();
52
+ const optionalCoercedNumber = nullableOptional(z.coerce.number());
53
+ const optionalCoercedBoolean = nullableOptional(z.coerce.boolean());
54
+ const workItemReferenceSchema = z.object({
55
+ project_id: projectIdSchema,
56
+ iid: workItemIidSchema
57
+ });
58
+ const linkedWorkItemReferenceSchema = z.object({
59
+ project_id: projectIdSchema,
60
+ iid: workItemIidSchema,
61
+ link_type: z.enum(["RELATED", "BLOCKED_BY", "BLOCKS"]).optional()
62
+ });
63
+ const customFieldValueSchema = z.object({
64
+ custom_field_id: z.string().min(1),
65
+ text_value: optionalString,
66
+ number_value: optionalCoercedNumber,
67
+ selected_option_ids: optionalStringArray,
68
+ date_value: optionalString
69
+ });
33
70
  export function registerGitLabTools(server, context) {
34
71
  const definitions = getGitLabToolDefinitions();
35
72
  const disableGraphqlTools = shouldDisableGraphqlTools(context.env.GITLAB_ALLOWED_PROJECT_IDS, context.env.GITLAB_ALLOW_GRAPHQL_WITH_PROJECT_SCOPE);
@@ -113,6 +150,7 @@ function getGitLabToolDefinitions() {
113
150
  capabilities: readCapabilities,
114
151
  inputSchema: {
115
152
  search: optionalString,
153
+ topic: optionalString,
116
154
  search_namespaces: optionalBoolean,
117
155
  membership: optionalBoolean,
118
156
  owned: optionalBoolean,
@@ -154,6 +192,26 @@ function getGitLabToolDefinitions() {
154
192
  default_branch: getOptionalString(args, "default_branch")
155
193
  })
156
194
  },
195
+ {
196
+ name: "gitlab_create_group",
197
+ title: "Create Group",
198
+ description: "Create a new GitLab group or subgroup.",
199
+ capabilities: adminCapabilities,
200
+ inputSchema: {
201
+ name: displayNameSchema,
202
+ path: z.string().min(1),
203
+ description: optionalString,
204
+ visibility: z.enum(["private", "internal", "public"]).optional(),
205
+ parent_id: optionalNumber
206
+ },
207
+ handler: async (args, context) => context.gitlab.createGroup({
208
+ name: getString(args, "name"),
209
+ path: getString(args, "path"),
210
+ description: getOptionalString(args, "description"),
211
+ visibility: getOptionalString(args, "visibility"),
212
+ parent_id: getOptionalNumber(args, "parent_id")
213
+ })
214
+ },
157
215
  {
158
216
  name: "gitlab_list_project_members",
159
217
  title: "List Project Members",
@@ -183,6 +241,7 @@ function getGitLabToolDefinitions() {
183
241
  group_id: z.string(),
184
242
  include_subgroups: optionalBoolean,
185
243
  search: optionalString,
244
+ topic: optionalString,
186
245
  order_by: z
187
246
  .enum(["name", "path", "created_at", "updated_at", "last_activity_at"])
188
247
  .optional(),
@@ -253,10 +312,62 @@ function getGitLabToolDefinitions() {
253
312
  project_id: optionalProjectIdSchema,
254
313
  search: z.string().min(1),
255
314
  ref: optionalRefLikeSchema,
315
+ filename: optionalString,
316
+ path: optionalString,
317
+ extension: optionalString,
318
+ ...paginationShape
319
+ },
320
+ handler: async (args, context) => context.gitlab.searchCodeBlobs(resolveProjectId(args, context, true), getString(args, "search"), { query: toQuery(omit(args, ["project_id", "search"])) })
321
+ },
322
+ {
323
+ name: "gitlab_search_code",
324
+ title: "Search Code",
325
+ description: "Search code across all projects on the GitLab instance. Requires GitLab code search support.",
326
+ capabilities: readCapabilities,
327
+ inputSchema: {
328
+ search: z.string().min(1),
329
+ filename: optionalString,
330
+ path: optionalString,
331
+ extension: optionalString,
332
+ ...paginationShape
333
+ },
334
+ handler: async (args, context) => context.gitlab.searchCode(getString(args, "search"), {
335
+ query: toQuery(omit(args, ["search"]))
336
+ })
337
+ },
338
+ {
339
+ name: "gitlab_search_project_code",
340
+ title: "Search Project Code",
341
+ description: "Search code in a specific project.",
342
+ capabilities: readCapabilities,
343
+ inputSchema: {
344
+ project_id: optionalProjectIdSchema,
345
+ search: z.string().min(1),
346
+ ref: optionalRefLikeSchema,
347
+ filename: optionalString,
348
+ path: optionalString,
349
+ extension: optionalString,
256
350
  ...paginationShape
257
351
  },
258
352
  handler: async (args, context) => context.gitlab.searchCodeBlobs(resolveProjectId(args, context, true), getString(args, "search"), { query: toQuery(omit(args, ["project_id", "search"])) })
259
353
  },
354
+ {
355
+ name: "gitlab_search_group_code",
356
+ title: "Search Group Code",
357
+ description: "Search code in a specific group.",
358
+ capabilities: readCapabilities,
359
+ inputSchema: {
360
+ group_id: projectIdSchema,
361
+ search: z.string().min(1),
362
+ filename: optionalString,
363
+ path: optionalString,
364
+ extension: optionalString,
365
+ ...paginationShape
366
+ },
367
+ handler: async (args, context) => context.gitlab.searchGroupCodeBlobs(getString(args, "group_id"), getString(args, "search"), {
368
+ query: toQuery(omit(args, ["group_id", "search"]))
369
+ })
370
+ },
260
371
  {
261
372
  name: "gitlab_get_repository_tree",
262
373
  title: "Get Repository Tree",
@@ -284,7 +395,8 @@ function getGitLabToolDefinitions() {
284
395
  inputSchema: {
285
396
  project_id: optionalProjectIdSchema,
286
397
  file_path: z.string().min(1),
287
- ref: optionalRefLikeSchema
398
+ ref: optionalRefLikeSchema,
399
+ decode_base64: optionalBoolean
288
400
  },
289
401
  handler: async (args, context) => {
290
402
  const projectId = resolveProjectId(args, context, true);
@@ -293,7 +405,37 @@ function getGitLabToolDefinitions() {
293
405
  const project = (await context.gitlab.getProject(projectId));
294
406
  ref = typeof project.default_branch === "string" ? project.default_branch : "main";
295
407
  }
296
- return context.gitlab.getFileContents(projectId, getString(args, "file_path"), ref);
408
+ const result = await context.gitlab.getFileContents(projectId, getString(args, "file_path"), ref);
409
+ return maybeDecodeRepositoryFileContents(result, getOptionalBoolean(args, "decode_base64"));
410
+ }
411
+ },
412
+ {
413
+ name: "gitlab_get_file_blame",
414
+ title: "Get File Blame",
415
+ description: "Get git blame for a repository file at a given ref. Optional line range must provide both range_start and range_end.",
416
+ capabilities: readCapabilities,
417
+ inputSchema: {
418
+ project_id: optionalProjectIdSchema,
419
+ file_path: z.string().min(1),
420
+ ref: refLikeSchema,
421
+ range_start: z.number().int().positive().optional(),
422
+ range_end: z.number().int().positive().optional()
423
+ },
424
+ handler: async (args, context) => {
425
+ const rangeStart = getOptionalNumber(args, "range_start");
426
+ const rangeEnd = getOptionalNumber(args, "range_end");
427
+ if ((rangeStart === undefined) !== (rangeEnd === undefined)) {
428
+ throw new Error("range_start and range_end must be provided together");
429
+ }
430
+ if (rangeStart !== undefined && rangeEnd !== undefined && rangeStart > rangeEnd) {
431
+ throw new Error("range_start must be less than or equal to range_end");
432
+ }
433
+ return context.gitlab.getFileBlame(resolveProjectId(args, context, true), getString(args, "file_path"), getString(args, "ref"), {
434
+ query: toQuery({
435
+ "range[start]": rangeStart,
436
+ "range[end]": rangeEnd
437
+ })
438
+ });
297
439
  }
298
440
  },
299
441
  {
@@ -417,6 +559,44 @@ function getGitLabToolDefinitions() {
417
559
  });
418
560
  }
419
561
  },
562
+ {
563
+ name: "gitlab_list_branches",
564
+ title: "List Branches",
565
+ description: "List repository branches.",
566
+ capabilities: readCapabilities,
567
+ inputSchema: {
568
+ project_id: optionalProjectIdSchema,
569
+ search: optionalString,
570
+ regex: optionalString,
571
+ sort: z.enum(["name_asc", "updated_asc", "updated_desc"]).optional(),
572
+ ...paginationShape
573
+ },
574
+ handler: async (args, context) => context.gitlab.listBranches(resolveProjectId(args, context, true), {
575
+ query: toQuery(omit(args, ["project_id"]))
576
+ })
577
+ },
578
+ {
579
+ name: "gitlab_get_branch",
580
+ title: "Get Branch",
581
+ description: "Get details for one repository branch.",
582
+ capabilities: readCapabilities,
583
+ inputSchema: {
584
+ project_id: optionalProjectIdSchema,
585
+ branch: refLikeSchema
586
+ },
587
+ handler: async (args, context) => context.gitlab.getBranch(resolveProjectId(args, context, true), getString(args, "branch"))
588
+ },
589
+ {
590
+ name: "gitlab_delete_branch",
591
+ title: "Delete Branch",
592
+ description: "Delete a repository branch permanently. Requires branch. Recommended pre-check: gitlab_get_branch.",
593
+ capabilities: deleteCapabilities,
594
+ inputSchema: {
595
+ project_id: optionalProjectIdSchema,
596
+ branch: refLikeSchema
597
+ },
598
+ handler: async (args, context) => context.gitlab.deleteBranch(resolveProjectId(args, context, true), getString(args, "branch"))
599
+ },
420
600
  {
421
601
  name: "gitlab_get_branch_diffs",
422
602
  title: "Get Branch Diffs",
@@ -502,6 +682,49 @@ function getGitLabToolDefinitions() {
502
682
  });
503
683
  }
504
684
  },
685
+ {
686
+ name: "gitlab_list_commit_statuses",
687
+ title: "List Commit Statuses",
688
+ description: "List statuses for a commit.",
689
+ capabilities: readCapabilities,
690
+ inputSchema: {
691
+ project_id: optionalProjectIdSchema,
692
+ sha: z.string().min(1),
693
+ ref: optionalRefLikeSchema,
694
+ stage: optionalString,
695
+ name: optionalString,
696
+ pipeline_id: optionalNumber,
697
+ order_by: z.enum(["id", "pipeline_id"]).optional(),
698
+ sort: z.enum(["asc", "desc"]).optional(),
699
+ all: optionalBoolean,
700
+ ...paginationShape
701
+ },
702
+ handler: async (args, context) => context.gitlab.listCommitStatuses(resolveProjectId(args, context, true), getString(args, "sha"), { query: toQuery(omit(args, ["project_id", "sha"])) })
703
+ },
704
+ {
705
+ name: "gitlab_create_commit_status",
706
+ title: "Create Commit Status",
707
+ description: "Create or update the status of a commit.",
708
+ capabilities: writeCapabilities,
709
+ inputSchema: {
710
+ project_id: optionalProjectIdSchema,
711
+ sha: z.string().min(1),
712
+ state: z.enum(["pending", "running", "success", "failed", "canceled", "skipped"]),
713
+ ref: optionalRefLikeSchema,
714
+ name: optionalString,
715
+ context: optionalString,
716
+ target_url: optionalString,
717
+ description: optionalString,
718
+ coverage: optionalNumber,
719
+ pipeline_id: optionalNumber
720
+ },
721
+ handler: async (args, context) => {
722
+ if (getOptionalString(args, "name") && getOptionalString(args, "context")) {
723
+ throw new Error("Use either name or context when creating a commit status, not both");
724
+ }
725
+ return context.gitlab.createCommitStatus(resolveProjectId(args, context, true), getString(args, "sha"), toQuery(omit(args, ["project_id", "sha"])));
726
+ }
727
+ },
505
728
  {
506
729
  name: "gitlab_list_merge_requests",
507
730
  title: "List Merge Requests",
@@ -564,7 +787,8 @@ function getGitLabToolDefinitions() {
564
787
  const projectId = resolveProjectId(args, context, true);
565
788
  const mergeRequestIid = getOptionalString(args, "merge_request_iid");
566
789
  if (mergeRequestIid) {
567
- return context.gitlab.getMergeRequest(projectId, mergeRequestIid);
790
+ const mergeRequest = await context.gitlab.getMergeRequest(projectId, mergeRequestIid);
791
+ return withMergeRequestSummaries(projectId, mergeRequest, context);
568
792
  }
569
793
  const sourceBranch = getOptionalString(args, "source_branch");
570
794
  if (!sourceBranch) {
@@ -580,9 +804,22 @@ function getGitLabToolDefinitions() {
580
804
  const match = pickMergeRequestForSourceBranch(candidates, sourceBranch, {
581
805
  requireOpened: false
582
806
  });
583
- return match;
807
+ const mergeRequest = await getDetailedMergeRequestFromMatch(projectId, match, context);
808
+ return withMergeRequestSummaries(projectId, mergeRequest, context);
584
809
  }
585
810
  },
811
+ {
812
+ name: "gitlab_list_merge_request_pipelines",
813
+ title: "List Merge Request Pipelines",
814
+ description: "List pipelines associated with a merge request.",
815
+ capabilities: readCapabilities,
816
+ inputSchema: {
817
+ project_id: optionalProjectIdSchema,
818
+ merge_request_iid: z.string().min(1),
819
+ ...paginationShape
820
+ },
821
+ handler: async (args, context) => context.gitlab.listMergeRequestPipelines(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
822
+ },
586
823
  {
587
824
  name: "gitlab_create_merge_request",
588
825
  title: "Create Merge Request",
@@ -739,6 +976,33 @@ function getGitLabToolDefinitions() {
739
976
  },
740
977
  handler: async (args, context) => context.gitlab.getMergeRequestDiffs(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
741
978
  },
979
+ {
980
+ name: "gitlab_list_merge_request_changed_files",
981
+ title: "List Merge Request Changed Files",
982
+ description: "Step 1 for large MR review: return changed file metadata without diff content.",
983
+ capabilities: readCapabilities,
984
+ inputSchema: {
985
+ project_id: optionalProjectIdSchema,
986
+ merge_request_iid: optionalString,
987
+ source_branch: optionalRefLikeSchema,
988
+ excluded_file_patterns: optionalStringArray
989
+ },
990
+ handler: async (args, context) => {
991
+ const projectId = resolveProjectId(args, context, true);
992
+ const mergeRequestIid = await resolveMergeRequestIid(args, context, projectId, {
993
+ requireOpened: false
994
+ });
995
+ const response = await context.gitlab.getMergeRequestDiffs(projectId, mergeRequestIid);
996
+ const files = extractMergeRequestChanges(response).map((item) => ({
997
+ new_path: item.new_path,
998
+ old_path: item.old_path,
999
+ new_file: item.new_file,
1000
+ deleted_file: item.deleted_file,
1001
+ renamed_file: item.renamed_file
1002
+ }));
1003
+ return filterChangedFiles(files, getOptionalStringArray(args, "excluded_file_patterns"));
1004
+ }
1005
+ },
742
1006
  {
743
1007
  name: "gitlab_list_merge_request_diffs",
744
1008
  title: "List Merge Request Diffs",
@@ -753,6 +1017,67 @@ function getGitLabToolDefinitions() {
753
1017
  },
754
1018
  handler: async (args, context) => context.gitlab.listMergeRequestDiffs(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
755
1019
  },
1020
+ {
1021
+ name: "gitlab_get_merge_request_file_diff",
1022
+ title: "Get Merge Request File Diff",
1023
+ description: "Step 2 for large MR review: fetch diffs for specific files from a merge request.",
1024
+ capabilities: readCapabilities,
1025
+ inputSchema: {
1026
+ project_id: optionalProjectIdSchema,
1027
+ merge_request_iid: optionalString,
1028
+ source_branch: optionalRefLikeSchema,
1029
+ file_paths: z.array(z.string().min(1)).min(1),
1030
+ unidiff: optionalBoolean
1031
+ },
1032
+ handler: async (args, context) => {
1033
+ const projectId = resolveProjectId(args, context, true);
1034
+ const mergeRequestIid = await resolveMergeRequestIid(args, context, projectId, {
1035
+ requireOpened: false
1036
+ });
1037
+ const requested = getRequiredStringArray(args, "file_paths");
1038
+ const remaining = new Set(requested);
1039
+ const results = [];
1040
+ let page = 1;
1041
+ const perPage = 20;
1042
+ while (remaining.size > 0) {
1043
+ const pageItems = extractMergeRequestDiffRecords(await context.gitlab.listMergeRequestDiffs(projectId, mergeRequestIid, {
1044
+ query: toQuery({
1045
+ page,
1046
+ per_page: perPage,
1047
+ unidiff: getOptionalBoolean(args, "unidiff")
1048
+ })
1049
+ }));
1050
+ if (pageItems.length === 0) {
1051
+ break;
1052
+ }
1053
+ for (const item of pageItems) {
1054
+ const newPath = typeof item.new_path === "string" ? item.new_path : undefined;
1055
+ const oldPath = typeof item.old_path === "string" ? item.old_path : undefined;
1056
+ if ((newPath && remaining.has(newPath)) || (oldPath && remaining.has(oldPath))) {
1057
+ results.push(item);
1058
+ if (newPath) {
1059
+ remaining.delete(newPath);
1060
+ }
1061
+ if (oldPath) {
1062
+ remaining.delete(oldPath);
1063
+ }
1064
+ }
1065
+ }
1066
+ if (pageItems.length < perPage) {
1067
+ break;
1068
+ }
1069
+ page += 1;
1070
+ }
1071
+ for (const missing of remaining) {
1072
+ results.push({
1073
+ file_path: missing,
1074
+ error: `File not found in merge request diffs: ${missing}`,
1075
+ hint: "Use gitlab_list_merge_request_changed_files to verify the correct file paths."
1076
+ });
1077
+ }
1078
+ return results;
1079
+ }
1080
+ },
756
1081
  {
757
1082
  name: "gitlab_get_merge_request_code_context",
758
1083
  title: "Get Merge Request Code Context",
@@ -1150,6 +1475,92 @@ function getGitLabToolDefinitions() {
1150
1475
  },
1151
1476
  handler: async (args, context) => context.gitlab.deleteMergeRequestNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"))
1152
1477
  },
1478
+ {
1479
+ name: "gitlab_list_merge_request_emoji_reactions",
1480
+ title: "List Merge Request Emoji Reactions",
1481
+ description: "List emoji reactions on a merge request.",
1482
+ capabilities: readCapabilities,
1483
+ inputSchema: {
1484
+ project_id: optionalProjectIdSchema,
1485
+ merge_request_iid: z.string().min(1),
1486
+ ...paginationShape
1487
+ },
1488
+ handler: async (args, context) => context.gitlab.listMergeRequestEmojiReactions(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
1489
+ },
1490
+ {
1491
+ name: "gitlab_list_merge_request_note_emoji_reactions",
1492
+ title: "List MR Note Emoji Reactions",
1493
+ description: "List emoji reactions on a merge request note. Pass discussion_id for discussion replies.",
1494
+ capabilities: readCapabilities,
1495
+ inputSchema: {
1496
+ project_id: optionalProjectIdSchema,
1497
+ merge_request_iid: z.string().min(1),
1498
+ note_id: z.string().min(1),
1499
+ discussion_id: optionalString,
1500
+ ...paginationShape
1501
+ },
1502
+ handler: async (args, context) => context.gitlab.listMergeRequestNoteEmojiReactions(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"), { discussion_id: getOptionalString(args, "discussion_id") }, {
1503
+ query: toQuery(omit(args, ["project_id", "merge_request_iid", "note_id", "discussion_id"]))
1504
+ })
1505
+ },
1506
+ {
1507
+ name: "gitlab_create_merge_request_emoji_reaction",
1508
+ title: "Create Merge Request Emoji Reaction",
1509
+ description: "Add an emoji reaction to a merge request, for example thumbsup, rocket, or eyes.",
1510
+ capabilities: writeCapabilities,
1511
+ inputSchema: {
1512
+ project_id: optionalProjectIdSchema,
1513
+ merge_request_iid: z.string().min(1),
1514
+ name: emojiNameSchema
1515
+ },
1516
+ handler: async (args, context) => context.gitlab.createMergeRequestEmojiReaction(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "name"))
1517
+ },
1518
+ {
1519
+ name: "gitlab_delete_merge_request_emoji_reaction",
1520
+ title: "Delete Merge Request Emoji Reaction",
1521
+ description: "Delete an emoji reaction from a merge request permanently. Irreversible for that reaction. Requires merge_request_iid and award_id. Recommended pre-check: gitlab_list_merge_request_emoji_reactions.",
1522
+ capabilities: deleteCapabilities,
1523
+ inputSchema: {
1524
+ project_id: optionalProjectIdSchema,
1525
+ merge_request_iid: z.string().min(1),
1526
+ award_id: awardEmojiIdSchema
1527
+ },
1528
+ handler: async (args, context) => context.gitlab.deleteMergeRequestEmojiReaction(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "award_id"))
1529
+ },
1530
+ {
1531
+ name: "gitlab_create_merge_request_note_emoji_reaction",
1532
+ title: "Create MR Note Emoji Reaction",
1533
+ description: "Add an emoji reaction to a merge request note. Pass discussion_id for discussion replies.",
1534
+ capabilities: writeCapabilities,
1535
+ inputSchema: {
1536
+ project_id: optionalProjectIdSchema,
1537
+ merge_request_iid: z.string().min(1),
1538
+ note_id: z.string().min(1),
1539
+ discussion_id: optionalString,
1540
+ name: emojiNameSchema
1541
+ },
1542
+ handler: async (args, context) => context.gitlab.createMergeRequestNoteEmojiReaction(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"), {
1543
+ name: getString(args, "name"),
1544
+ discussion_id: getOptionalString(args, "discussion_id")
1545
+ })
1546
+ },
1547
+ {
1548
+ name: "gitlab_delete_merge_request_note_emoji_reaction",
1549
+ title: "Delete MR Note Emoji Reaction",
1550
+ description: "Delete an emoji reaction from a merge request note permanently. Irreversible for that reaction. Requires merge_request_iid, note_id, and award_id. Recommended pre-check: gitlab_list_merge_request_note_emoji_reactions.",
1551
+ capabilities: deleteCapabilities,
1552
+ inputSchema: {
1553
+ project_id: optionalProjectIdSchema,
1554
+ merge_request_iid: z.string().min(1),
1555
+ note_id: z.string().min(1),
1556
+ discussion_id: optionalString,
1557
+ award_id: awardEmojiIdSchema
1558
+ },
1559
+ handler: async (args, context) => context.gitlab.deleteMergeRequestNoteEmojiReaction(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"), {
1560
+ award_id: getString(args, "award_id"),
1561
+ discussion_id: getOptionalString(args, "discussion_id")
1562
+ })
1563
+ },
1153
1564
  {
1154
1565
  name: "gitlab_list_issues",
1155
1566
  title: "List Issues",
@@ -1211,6 +1622,71 @@ function getGitLabToolDefinitions() {
1211
1622
  });
1212
1623
  }
1213
1624
  },
1625
+ {
1626
+ name: "gitlab_list_todos",
1627
+ title: "List Todos",
1628
+ description: "List to-do items for the current authenticated user.",
1629
+ capabilities: readCapabilities,
1630
+ inputSchema: {
1631
+ action: z
1632
+ .enum([
1633
+ "assigned",
1634
+ "mentioned",
1635
+ "build_failed",
1636
+ "marked",
1637
+ "approval_required",
1638
+ "unmergeable",
1639
+ "directly_addressed",
1640
+ "merge_train_removed",
1641
+ "member_access_requested"
1642
+ ])
1643
+ .optional(),
1644
+ author_id: optionalNumber,
1645
+ project_id: optionalNumber,
1646
+ group_id: optionalNumber,
1647
+ state: z.enum(["pending", "done"]).optional(),
1648
+ type: z
1649
+ .enum([
1650
+ "Issue",
1651
+ "MergeRequest",
1652
+ "Commit",
1653
+ "Epic",
1654
+ "DesignManagement::Design",
1655
+ "AlertManagement::Alert",
1656
+ "Project",
1657
+ "Namespace",
1658
+ "Vulnerability",
1659
+ "WikiPage::Meta"
1660
+ ])
1661
+ .optional(),
1662
+ ...paginationShape
1663
+ },
1664
+ handler: async (args, context) => context.gitlab.listTodos({ query: toQuery(args) })
1665
+ },
1666
+ {
1667
+ name: "gitlab_mark_todo_done",
1668
+ title: "Mark Todo Done",
1669
+ description: "Mark one to-do item as done.",
1670
+ capabilities: writeCapabilities,
1671
+ inputSchema: {
1672
+ todo_id: z.string().min(1)
1673
+ },
1674
+ handler: async (args, context) => context.gitlab.markTodoDone(getString(args, "todo_id"))
1675
+ },
1676
+ {
1677
+ name: "gitlab_mark_all_todos_done",
1678
+ title: "Mark All Todos Done",
1679
+ description: "Mark all pending to-do items as done for the current authenticated user.",
1680
+ capabilities: writeCapabilities,
1681
+ inputSchema: {},
1682
+ handler: async (_args, context) => {
1683
+ await context.gitlab.markAllTodosDone();
1684
+ return {
1685
+ status: "success",
1686
+ message: "All pending to-do items marked as done"
1687
+ };
1688
+ }
1689
+ },
1214
1690
  {
1215
1691
  name: "gitlab_get_issue",
1216
1692
  title: "Get Issue",
@@ -1266,7 +1742,7 @@ function getGitLabToolDefinitions() {
1266
1742
  confidential: optionalBoolean,
1267
1743
  assignee_ids: optionalNumberArray,
1268
1744
  discussion_locked: optionalBoolean,
1269
- weight: optionalNumber,
1745
+ weight: optionalCoercedNumber,
1270
1746
  issue_type: z.enum(["issue", "incident", "test_case", "task"]).optional()
1271
1747
  },
1272
1748
  handler: async (args, context) => {
@@ -1280,6 +1756,79 @@ function getGitLabToolDefinitions() {
1280
1756
  return context.gitlab.updateIssue(resolveProjectId(args, context, true), getString(args, "issue_iid"), payload);
1281
1757
  }
1282
1758
  },
1759
+ {
1760
+ name: "gitlab_update_issue_description_patch",
1761
+ title: "Update Issue Description Patch",
1762
+ description: "Apply a search/replace or unified diff patch to an issue description without sending the full replacement text.",
1763
+ capabilities: writeCapabilities,
1764
+ inputSchema: {
1765
+ project_id: optionalProjectIdSchema,
1766
+ issue_iid: z.string().min(1),
1767
+ patch_type: z.enum(["search_replace", "unified_diff"]),
1768
+ patch: z.string().min(1).max(50_000),
1769
+ dry_run: optionalBoolean,
1770
+ create_note: optionalBoolean,
1771
+ allow_multiple: optionalBoolean
1772
+ },
1773
+ handler: async (args, context) => {
1774
+ const projectId = resolveProjectId(args, context, true);
1775
+ const issueIid = getString(args, "issue_iid");
1776
+ const issue = (await context.gitlab.getIssue(projectId, issueIid));
1777
+ const currentDescription = typeof issue.description === "string" ? issue.description : "";
1778
+ const patchType = getString(args, "patch_type");
1779
+ const patch = getString(args, "patch");
1780
+ let result;
1781
+ if (patchType === "search_replace") {
1782
+ const blocks = parseSearchReplaceBlocks(patch);
1783
+ if (blocks.length === 0) {
1784
+ throw new Error("No valid search/replace blocks found. Expected format: <<<<<<< SEARCH\\ntext\\n=======\\nnew text\\n>>>>>>> REPLACE");
1785
+ }
1786
+ result = applySearchReplace(currentDescription, blocks, getOptionalBoolean(args, "allow_multiple") ?? false);
1787
+ }
1788
+ else {
1789
+ result = applyUnifiedDiff(currentDescription, patch);
1790
+ }
1791
+ if (getOptionalBoolean(args, "dry_run")) {
1792
+ return {
1793
+ status: "preview",
1794
+ dry_run: true,
1795
+ changes: result.changes,
1796
+ summary: result.summary,
1797
+ preview: result.preview
1798
+ };
1799
+ }
1800
+ const updatedIssue = (await context.gitlab.updateIssue(projectId, issueIid, {
1801
+ description: result.description
1802
+ }));
1803
+ let note;
1804
+ if (getOptionalBoolean(args, "create_note")) {
1805
+ try {
1806
+ await context.gitlab.createIssueNote(projectId, issueIid, {
1807
+ body: `Updated issue description using patch-based tool.\n\n${result.summary}`
1808
+ });
1809
+ note = { status: "created" };
1810
+ }
1811
+ catch (error) {
1812
+ note = {
1813
+ status: "failed",
1814
+ message: error instanceof Error ? error.message : String(error)
1815
+ };
1816
+ }
1817
+ }
1818
+ return {
1819
+ status: "success",
1820
+ changes: result.changes,
1821
+ summary: result.summary,
1822
+ note,
1823
+ issue: {
1824
+ iid: updatedIssue.iid,
1825
+ title: updatedIssue.title,
1826
+ web_url: updatedIssue.web_url,
1827
+ updated_at: updatedIssue.updated_at
1828
+ }
1829
+ };
1830
+ }
1831
+ },
1283
1832
  {
1284
1833
  name: "gitlab_delete_issue",
1285
1834
  title: "Delete Issue",
@@ -1347,25 +1896,109 @@ function getGitLabToolDefinitions() {
1347
1896
  }
1348
1897
  },
1349
1898
  {
1350
- name: "gitlab_list_issue_links",
1351
- title: "List Issue Links",
1352
- description: "List related issue links for an issue.",
1899
+ name: "gitlab_list_issue_emoji_reactions",
1900
+ title: "List Issue Emoji Reactions",
1901
+ description: "List emoji reactions on an issue.",
1353
1902
  capabilities: readCapabilities,
1354
1903
  inputSchema: {
1355
1904
  project_id: optionalProjectIdSchema,
1356
- issue_iid: z.string().min(1)
1905
+ issue_iid: z.string().min(1),
1906
+ ...paginationShape
1357
1907
  },
1358
- handler: async (args, context) => context.gitlab.listIssueLinks(resolveProjectId(args, context, true), getString(args, "issue_iid"))
1908
+ handler: async (args, context) => context.gitlab.listIssueEmojiReactions(resolveProjectId(args, context, true), getString(args, "issue_iid"), { query: toQuery(omit(args, ["project_id", "issue_iid"])) })
1359
1909
  },
1360
1910
  {
1361
- name: "gitlab_get_issue_link",
1362
- title: "Get Issue Link",
1363
- description: "Get a single issue link by ID.",
1911
+ name: "gitlab_list_issue_note_emoji_reactions",
1912
+ title: "List Issue Note Emoji Reactions",
1913
+ description: "List emoji reactions on an issue note. Pass discussion_id for discussion replies.",
1364
1914
  capabilities: readCapabilities,
1365
1915
  inputSchema: {
1366
1916
  project_id: optionalProjectIdSchema,
1367
1917
  issue_iid: z.string().min(1),
1368
- issue_link_id: z.string().min(1)
1918
+ note_id: z.string().min(1),
1919
+ discussion_id: optionalString,
1920
+ ...paginationShape
1921
+ },
1922
+ handler: async (args, context) => context.gitlab.listIssueNoteEmojiReactions(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "note_id"), { discussion_id: getOptionalString(args, "discussion_id") }, { query: toQuery(omit(args, ["project_id", "issue_iid", "note_id", "discussion_id"])) })
1923
+ },
1924
+ {
1925
+ name: "gitlab_create_issue_emoji_reaction",
1926
+ title: "Create Issue Emoji Reaction",
1927
+ description: "Add an emoji reaction to an issue, for example thumbsup, rocket, or eyes.",
1928
+ capabilities: writeCapabilities,
1929
+ inputSchema: {
1930
+ project_id: optionalProjectIdSchema,
1931
+ issue_iid: z.string().min(1),
1932
+ name: emojiNameSchema
1933
+ },
1934
+ handler: async (args, context) => context.gitlab.createIssueEmojiReaction(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "name"))
1935
+ },
1936
+ {
1937
+ name: "gitlab_delete_issue_emoji_reaction",
1938
+ title: "Delete Issue Emoji Reaction",
1939
+ description: "Delete an emoji reaction from an issue permanently. Irreversible for that reaction. Requires issue_iid and award_id. Recommended pre-check: gitlab_list_issue_emoji_reactions.",
1940
+ capabilities: deleteCapabilities,
1941
+ inputSchema: {
1942
+ project_id: optionalProjectIdSchema,
1943
+ issue_iid: z.string().min(1),
1944
+ award_id: awardEmojiIdSchema
1945
+ },
1946
+ handler: async (args, context) => context.gitlab.deleteIssueEmojiReaction(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "award_id"))
1947
+ },
1948
+ {
1949
+ name: "gitlab_create_issue_note_emoji_reaction",
1950
+ title: "Create Issue Note Emoji Reaction",
1951
+ description: "Add an emoji reaction to an issue note. Pass discussion_id for discussion replies.",
1952
+ capabilities: writeCapabilities,
1953
+ inputSchema: {
1954
+ project_id: optionalProjectIdSchema,
1955
+ issue_iid: z.string().min(1),
1956
+ note_id: z.string().min(1),
1957
+ discussion_id: optionalString,
1958
+ name: emojiNameSchema
1959
+ },
1960
+ handler: async (args, context) => context.gitlab.createIssueNoteEmojiReaction(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "note_id"), {
1961
+ name: getString(args, "name"),
1962
+ discussion_id: getOptionalString(args, "discussion_id")
1963
+ })
1964
+ },
1965
+ {
1966
+ name: "gitlab_delete_issue_note_emoji_reaction",
1967
+ title: "Delete Issue Note Emoji Reaction",
1968
+ description: "Delete an emoji reaction from an issue note permanently. Irreversible for that reaction. Requires issue_iid, note_id, and award_id. Recommended pre-check: gitlab_list_issue_note_emoji_reactions.",
1969
+ capabilities: deleteCapabilities,
1970
+ inputSchema: {
1971
+ project_id: optionalProjectIdSchema,
1972
+ issue_iid: z.string().min(1),
1973
+ note_id: z.string().min(1),
1974
+ discussion_id: optionalString,
1975
+ award_id: awardEmojiIdSchema
1976
+ },
1977
+ handler: async (args, context) => context.gitlab.deleteIssueNoteEmojiReaction(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "note_id"), {
1978
+ award_id: getString(args, "award_id"),
1979
+ discussion_id: getOptionalString(args, "discussion_id")
1980
+ })
1981
+ },
1982
+ {
1983
+ name: "gitlab_list_issue_links",
1984
+ title: "List Issue Links",
1985
+ description: "List related issue links for an issue.",
1986
+ capabilities: readCapabilities,
1987
+ inputSchema: {
1988
+ project_id: optionalProjectIdSchema,
1989
+ issue_iid: z.string().min(1)
1990
+ },
1991
+ handler: async (args, context) => context.gitlab.listIssueLinks(resolveProjectId(args, context, true), getString(args, "issue_iid"))
1992
+ },
1993
+ {
1994
+ name: "gitlab_get_issue_link",
1995
+ title: "Get Issue Link",
1996
+ description: "Get a single issue link by ID.",
1997
+ capabilities: readCapabilities,
1998
+ inputSchema: {
1999
+ project_id: optionalProjectIdSchema,
2000
+ issue_iid: z.string().min(1),
2001
+ issue_link_id: z.string().min(1)
1369
2002
  },
1370
2003
  handler: async (args, context) => context.gitlab.getIssueLink(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "issue_link_id"))
1371
2004
  },
@@ -1478,6 +2111,87 @@ function getGitLabToolDefinitions() {
1478
2111
  },
1479
2112
  handler: async (args, context) => context.gitlab.deleteWikiPage(resolveProjectId(args, context, true), getString(args, "slug"))
1480
2113
  },
2114
+ {
2115
+ name: "gitlab_list_group_wiki_pages",
2116
+ title: "List Group Wiki Pages",
2117
+ description: "List wiki pages in a group.",
2118
+ capabilities: readCapabilities,
2119
+ requiresFeature: "wiki",
2120
+ inputSchema: {
2121
+ group_id: projectIdSchema,
2122
+ with_content: optionalBoolean,
2123
+ ...paginationShape
2124
+ },
2125
+ handler: async (args, context) => context.gitlab.listGroupWikiPages(getString(args, "group_id"), {
2126
+ query: toQuery(omit(args, ["group_id"]))
2127
+ })
2128
+ },
2129
+ {
2130
+ name: "gitlab_get_group_wiki_page",
2131
+ title: "Get Group Wiki Page",
2132
+ description: "Get group wiki page by slug.",
2133
+ capabilities: readCapabilities,
2134
+ requiresFeature: "wiki",
2135
+ inputSchema: {
2136
+ group_id: projectIdSchema,
2137
+ slug: slugSchema,
2138
+ version: optionalString
2139
+ },
2140
+ handler: async (args, context) => context.gitlab.getGroupWikiPage(getString(args, "group_id"), getString(args, "slug"), {
2141
+ query: toQuery(omit(args, ["group_id", "slug"]))
2142
+ })
2143
+ },
2144
+ {
2145
+ name: "gitlab_create_group_wiki_page",
2146
+ title: "Create Group Wiki Page",
2147
+ description: "Create a group wiki page.",
2148
+ capabilities: writeCapabilities,
2149
+ requiresFeature: "wiki",
2150
+ inputSchema: {
2151
+ group_id: projectIdSchema,
2152
+ title: z.string().min(1),
2153
+ content: z.string().min(1),
2154
+ format: optionalString
2155
+ },
2156
+ handler: async (args, context) => context.gitlab.createGroupWikiPage(getString(args, "group_id"), {
2157
+ title: getString(args, "title"),
2158
+ content: getString(args, "content"),
2159
+ format: getOptionalString(args, "format")
2160
+ })
2161
+ },
2162
+ {
2163
+ name: "gitlab_update_group_wiki_page",
2164
+ title: "Update Group Wiki Page",
2165
+ description: "Update group wiki page by slug.",
2166
+ capabilities: writeCapabilities,
2167
+ requiresFeature: "wiki",
2168
+ inputSchema: {
2169
+ group_id: projectIdSchema,
2170
+ slug: slugSchema,
2171
+ title: optionalString,
2172
+ content: optionalString,
2173
+ format: optionalString
2174
+ },
2175
+ handler: async (args, context) => {
2176
+ const payload = toQuery(omit(args, ["group_id", "slug"]));
2177
+ if (Object.keys(payload).length === 0) {
2178
+ throw new Error("At least one of title, content, or format must be provided");
2179
+ }
2180
+ return context.gitlab.updateGroupWikiPage(getString(args, "group_id"), getString(args, "slug"), payload);
2181
+ }
2182
+ },
2183
+ {
2184
+ name: "gitlab_delete_group_wiki_page",
2185
+ title: "Delete Group Wiki Page",
2186
+ description: "Delete a group wiki page permanently. Irreversible. Requires group_id and slug. Recommended pre-check: gitlab_get_group_wiki_page or gitlab_list_group_wiki_pages.",
2187
+ capabilities: deleteCapabilities,
2188
+ requiresFeature: "wiki",
2189
+ inputSchema: {
2190
+ group_id: projectIdSchema,
2191
+ slug: slugSchema
2192
+ },
2193
+ handler: async (args, context) => context.gitlab.deleteGroupWikiPage(getString(args, "group_id"), getString(args, "slug"))
2194
+ },
1481
2195
  {
1482
2196
  name: "gitlab_list_pipelines",
1483
2197
  title: "List Pipelines",
@@ -1673,6 +2387,38 @@ function getGitLabToolDefinitions() {
1673
2387
  },
1674
2388
  handler: async (args, context) => context.gitlab.getPipelineJobOutput(resolveProjectId(args, context, true), getString(args, "job_id"))
1675
2389
  },
2390
+ {
2391
+ name: "gitlab_validate_ci_lint",
2392
+ title: "Validate CI Lint",
2393
+ description: "Validate provided GitLab CI/CD YAML content for a project.",
2394
+ capabilities: readCapabilities,
2395
+ requiresFeature: "pipeline",
2396
+ inputSchema: {
2397
+ project_id: optionalProjectIdSchema,
2398
+ content: z.string().min(1),
2399
+ dry_run: optionalBoolean,
2400
+ include_jobs: optionalBoolean,
2401
+ ref: optionalRefLikeSchema
2402
+ },
2403
+ handler: async (args, context) => context.gitlab.validateCiLint(resolveProjectId(args, context, true), toQuery(omit(args, ["project_id"])))
2404
+ },
2405
+ {
2406
+ name: "gitlab_validate_project_ci_lint",
2407
+ title: "Validate Project CI Lint",
2408
+ description: "Validate an existing project CI/CD configuration.",
2409
+ capabilities: readCapabilities,
2410
+ requiresFeature: "pipeline",
2411
+ inputSchema: {
2412
+ project_id: optionalProjectIdSchema,
2413
+ content_ref: optionalRefLikeSchema,
2414
+ dry_run: optionalBoolean,
2415
+ dry_run_ref: optionalRefLikeSchema,
2416
+ include_jobs: optionalBoolean
2417
+ },
2418
+ handler: async (args, context) => context.gitlab.validateProjectCiLint(resolveProjectId(args, context, true), {
2419
+ query: toQuery(omit(args, ["project_id"]))
2420
+ })
2421
+ },
1676
2422
  {
1677
2423
  name: "gitlab_list_job_artifacts",
1678
2424
  title: "List Job Artifacts",
@@ -1697,7 +2443,17 @@ function getGitLabToolDefinitions() {
1697
2443
  project_id: optionalProjectIdSchema,
1698
2444
  job_id: z.string().min(1)
1699
2445
  },
1700
- handler: async (args, context) => context.gitlab.downloadJobArtifacts(resolveProjectId(args, context, true), getString(args, "job_id"))
2446
+ handler: async (args, context) => {
2447
+ const projectId = resolveProjectId(args, context, true);
2448
+ const jobId = getString(args, "job_id");
2449
+ if (shouldReturnDownloadProxy(context)) {
2450
+ return buildDownloadProxyResult(context, {
2451
+ type: "job-artifacts",
2452
+ params: { project_id: projectId, job_id: jobId }
2453
+ }, `artifacts_job_${jobId}.zip`);
2454
+ }
2455
+ return context.gitlab.downloadJobArtifacts(projectId, jobId);
2456
+ }
1701
2457
  },
1702
2458
  {
1703
2459
  name: "gitlab_download_job_artifacts_local",
@@ -2075,7 +2831,84 @@ function getGitLabToolDefinitions() {
2075
2831
  tag_name: refLikeSchema,
2076
2832
  direct_asset_path: z.string().min(1)
2077
2833
  },
2078
- handler: async (args, context) => context.gitlab.downloadReleaseAsset(resolveProjectId(args, context, true), getString(args, "tag_name"), getString(args, "direct_asset_path"))
2834
+ handler: async (args, context) => {
2835
+ const projectId = resolveProjectId(args, context, true);
2836
+ const tagName = getString(args, "tag_name");
2837
+ const directAssetPath = getString(args, "direct_asset_path");
2838
+ if (shouldReturnDownloadProxy(context)) {
2839
+ return buildDownloadProxyResult(context, {
2840
+ type: "release-asset",
2841
+ params: {
2842
+ project_id: projectId,
2843
+ tag_name: tagName,
2844
+ direct_asset_path: directAssetPath
2845
+ }
2846
+ }, directAssetPath.split("/").pop() || directAssetPath);
2847
+ }
2848
+ return context.gitlab.downloadReleaseAsset(projectId, tagName, directAssetPath);
2849
+ }
2850
+ },
2851
+ {
2852
+ name: "gitlab_list_tags",
2853
+ title: "List Tags",
2854
+ description: "List repository tags for a project.",
2855
+ capabilities: readCapabilities,
2856
+ inputSchema: {
2857
+ project_id: optionalProjectIdSchema,
2858
+ order_by: z.enum(["name", "updated", "version"]).optional(),
2859
+ sort: z.enum(["asc", "desc"]).optional(),
2860
+ search: optionalString,
2861
+ ...paginationShape
2862
+ },
2863
+ handler: async (args, context) => context.gitlab.listTags(resolveProjectId(args, context, true), {
2864
+ query: toQuery(omit(args, ["project_id"]))
2865
+ })
2866
+ },
2867
+ {
2868
+ name: "gitlab_get_tag",
2869
+ title: "Get Tag",
2870
+ description: "Get a repository tag by name.",
2871
+ capabilities: readCapabilities,
2872
+ inputSchema: {
2873
+ project_id: optionalProjectIdSchema,
2874
+ tag_name: refLikeSchema
2875
+ },
2876
+ handler: async (args, context) => context.gitlab.getTag(resolveProjectId(args, context, true), getString(args, "tag_name"))
2877
+ },
2878
+ {
2879
+ name: "gitlab_create_tag",
2880
+ title: "Create Tag",
2881
+ description: "Create a repository tag from a branch, commit SHA, or another tag.",
2882
+ capabilities: writeCapabilities,
2883
+ inputSchema: {
2884
+ project_id: optionalProjectIdSchema,
2885
+ tag_name: refLikeSchema,
2886
+ ref: refLikeSchema,
2887
+ message: optionalString
2888
+ },
2889
+ handler: async (args, context) => context.gitlab.createTag(resolveProjectId(args, context, true), toQuery(omit(args, ["project_id"])))
2890
+ },
2891
+ {
2892
+ name: "gitlab_delete_tag",
2893
+ title: "Delete Tag",
2894
+ description: "Delete a repository tag permanently. Irreversible for tag_name. Requires tag_name. Recommended pre-check: gitlab_get_tag or gitlab_list_tags.",
2895
+ capabilities: deleteCapabilities,
2896
+ inputSchema: {
2897
+ project_id: optionalProjectIdSchema,
2898
+ tag_name: refLikeSchema
2899
+ },
2900
+ handler: async (args, context) => context.gitlab.deleteTag(resolveProjectId(args, context, true), getString(args, "tag_name"))
2901
+ },
2902
+ {
2903
+ name: "gitlab_get_tag_signature",
2904
+ title: "Get Tag Signature",
2905
+ description: "Get the X.509 signature for a signed repository tag.",
2906
+ capabilities: readCapabilities,
2907
+ inputSchema: {
2908
+ project_id: optionalProjectIdSchema,
2909
+ tag_name: refLikeSchema
2910
+ },
2911
+ handler: async (args, context) => context.gitlab.getTagSignature(resolveProjectId(args, context, true), getString(args, "tag_name"))
2079
2912
  },
2080
2913
  {
2081
2914
  name: "gitlab_list_labels",
@@ -2219,6 +3052,24 @@ function getGitLabToolDefinitions() {
2219
3052
  },
2220
3053
  handler: async (args, context) => context.gitlab.getUsers({ query: toQuery(args) })
2221
3054
  },
3055
+ {
3056
+ name: "gitlab_get_user",
3057
+ title: "Get User",
3058
+ description: "Get one user by ID.",
3059
+ capabilities: readCapabilities,
3060
+ inputSchema: {
3061
+ user_id: z.string().min(1)
3062
+ },
3063
+ handler: async (args, context) => context.gitlab.getUser(getString(args, "user_id"))
3064
+ },
3065
+ {
3066
+ name: "gitlab_whoami",
3067
+ title: "Who Am I",
3068
+ description: "Get the current authenticated user.",
3069
+ capabilities: readCapabilities,
3070
+ inputSchema: {},
3071
+ handler: async (_args, context) => context.gitlab.whoami()
3072
+ },
2222
3073
  {
2223
3074
  name: "gitlab_list_events",
2224
3075
  title: "List Events",
@@ -2253,6 +3104,69 @@ function getGitLabToolDefinitions() {
2253
3104
  query: toQuery(omit(args, ["project_id"]))
2254
3105
  })
2255
3106
  },
3107
+ {
3108
+ name: "gitlab_list_webhooks",
3109
+ title: "List Webhooks",
3110
+ description: "List configured webhooks for a project or group.",
3111
+ capabilities: readCapabilities,
3112
+ inputSchema: {
3113
+ project_id: optionalProjectIdSchema,
3114
+ group_id: optionalProjectIdSchema,
3115
+ ...paginationShape
3116
+ },
3117
+ handler: async (args, context) => context.gitlab.listWebhooks(resolveWebhookScope(args), {
3118
+ query: toQuery(omit(args, ["project_id", "group_id"]))
3119
+ })
3120
+ },
3121
+ {
3122
+ name: "gitlab_list_webhook_events",
3123
+ title: "List Webhook Events",
3124
+ description: "List recent webhook events for a project or group webhook. Use summary mode for overviews.",
3125
+ capabilities: readCapabilities,
3126
+ inputSchema: {
3127
+ project_id: optionalProjectIdSchema,
3128
+ group_id: optionalProjectIdSchema,
3129
+ hook_id: z.union([z.string(), z.number()]),
3130
+ status: optionalStringOrNumber,
3131
+ summary: optionalBoolean,
3132
+ page: optionalNumber,
3133
+ per_page: z.number().int().min(1).max(20).optional()
3134
+ },
3135
+ handler: async (args, context) => {
3136
+ const events = extractRecords(await context.gitlab.listWebhookEvents(resolveWebhookScope(args), getIdString(args, "hook_id"), {
3137
+ query: toQuery({
3138
+ status: args.status,
3139
+ page: args.page,
3140
+ per_page: args.per_page ?? 20
3141
+ })
3142
+ }));
3143
+ return getOptionalBoolean(args, "summary") ? summarizeWebhookEvents(events) : events;
3144
+ }
3145
+ },
3146
+ {
3147
+ name: "gitlab_get_webhook_event",
3148
+ title: "Get Webhook Event",
3149
+ description: "Find one webhook event by ID. Provide page when known, otherwise scans up to 500 recent events.",
3150
+ capabilities: readCapabilities,
3151
+ inputSchema: {
3152
+ project_id: optionalProjectIdSchema,
3153
+ group_id: optionalProjectIdSchema,
3154
+ hook_id: z.union([z.string(), z.number()]),
3155
+ event_id: z.union([z.string(), z.number()]),
3156
+ page: optionalNumber
3157
+ },
3158
+ handler: async (args, context) => {
3159
+ const event = await findWebhookEvent(context, resolveWebhookScope(args), getIdString(args, "hook_id"), getIdString(args, "event_id"), getOptionalNumber(args, "page"));
3160
+ if (event) {
3161
+ return event;
3162
+ }
3163
+ return {
3164
+ error: `Webhook event ${getIdString(args, "event_id")} not found ${getOptionalNumber(args, "page")
3165
+ ? `on page ${getOptionalNumber(args, "page")}`
3166
+ : "in the 500 most recent events"}`
3167
+ };
3168
+ }
3169
+ },
2256
3170
  {
2257
3171
  name: "gitlab_upload_markdown",
2258
3172
  title: "Upload Markdown",
@@ -2268,6 +3182,9 @@ function getGitLabToolDefinitions() {
2268
3182
  const projectId = resolveProjectId(args, context, true);
2269
3183
  const filePath = getOptionalString(args, "file_path");
2270
3184
  if (filePath) {
3185
+ if (!context.allowLocalFileTools) {
3186
+ throw new Error("file_path cannot be used over HTTP. Provide content and filename.");
3187
+ }
2271
3188
  return context.gitlab.uploadMarkdownFile(projectId, filePath);
2272
3189
  }
2273
3190
  const content = getOptionalString(args, "content");
@@ -2300,10 +3217,30 @@ function getGitLabToolDefinitions() {
2300
3217
  if (!upload) {
2301
3218
  throw new Error("In project-scoped mode, url_or_path must be a GitLab upload URL/path like '/uploads/<secret>/<filename>'");
2302
3219
  }
3220
+ if (shouldReturnDownloadProxy(context)) {
3221
+ return buildDownloadProxyResult(context, {
3222
+ type: "attachment",
3223
+ params: {
3224
+ project_id: projectId,
3225
+ secret: upload.secret,
3226
+ filename: upload.filename
3227
+ }
3228
+ }, upload.filename);
3229
+ }
2303
3230
  const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(upload.secret)}/${encodeURIComponent(upload.filename)}`;
2304
3231
  return context.gitlab.downloadAttachment(apiRelativePath);
2305
3232
  }
2306
3233
  if (projectId && upload) {
3234
+ if (shouldReturnDownloadProxy(context)) {
3235
+ return buildDownloadProxyResult(context, {
3236
+ type: "attachment",
3237
+ params: {
3238
+ project_id: projectId,
3239
+ secret: upload.secret,
3240
+ filename: upload.filename
3241
+ }
3242
+ }, upload.filename);
3243
+ }
2307
3244
  const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(upload.secret)}/${encodeURIComponent(upload.filename)}`;
2308
3245
  return context.gitlab.downloadAttachment(apiRelativePath);
2309
3246
  }
@@ -2316,52 +3253,389 @@ function getGitLabToolDefinitions() {
2316
3253
  }
2317
3254
  const projectId = resolveProjectId(args, context, true);
2318
3255
  const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(secret)}/${encodeURIComponent(filename)}`;
3256
+ if (shouldReturnDownloadProxy(context)) {
3257
+ return buildDownloadProxyResult(context, {
3258
+ type: "attachment",
3259
+ params: { project_id: projectId, secret, filename }
3260
+ }, filename);
3261
+ }
2319
3262
  return context.gitlab.downloadAttachment(apiRelativePath);
2320
3263
  }
2321
3264
  },
2322
3265
  {
2323
- name: "gitlab_execute_graphql_query",
2324
- title: "Execute GraphQL Query",
2325
- description: "Execute read-only GraphQL query.",
3266
+ name: "gitlab_get_work_item",
3267
+ title: "Get Work Item",
3268
+ description: "Get a single work item with full widget details including status, hierarchy, labels, assignees, linked items, custom fields, and development data.",
2326
3269
  capabilities: readGraphqlCapabilities,
2327
3270
  inputSchema: {
2328
- query: z.string().min(1),
2329
- variables: optionalRecord
3271
+ project_id: optionalProjectIdSchema,
3272
+ iid: workItemIidSchema
2330
3273
  },
2331
- handler: async (args, context) => {
2332
- const query = getString(args, "query");
2333
- if (containsGraphqlMutation(query)) {
2334
- throw new Error("Mutation detected. Use gitlab_execute_graphql_mutation for mutation operations.");
2335
- }
2336
- return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
2337
- }
3274
+ handler: async (args, context) => getWorkItem(context, resolveProjectId(args, context, true), getNumber(args, "iid"))
2338
3275
  },
2339
3276
  {
2340
- name: "gitlab_execute_graphql_mutation",
2341
- title: "Execute GraphQL Mutation",
2342
- description: "Execute GraphQL mutation (disabled in read-only mode).",
2343
- capabilities: writeGraphqlCapabilities,
3277
+ name: "gitlab_list_work_items",
3278
+ title: "List Work Items",
3279
+ description: "List work items in a project with filters for type, state, search, assignees, and labels.",
3280
+ capabilities: readGraphqlCapabilities,
2344
3281
  inputSchema: {
2345
- query: z.string().min(1),
2346
- variables: optionalRecord
3282
+ project_id: optionalProjectIdSchema,
3283
+ types: nullableOptional(z.array(workItemTypeSchema)),
3284
+ state: z.enum(["opened", "closed"]).optional(),
3285
+ search: optionalString,
3286
+ assignee_usernames: optionalStringArray,
3287
+ label_names: optionalStringArray,
3288
+ first: z.coerce.number().int().positive().max(100).optional(),
3289
+ after: optionalString
2347
3290
  },
2348
- handler: async (args, context) => {
2349
- const query = getString(args, "query");
2350
- if (!containsGraphqlMutation(query)) {
2351
- throw new Error("No mutation detected. Use gitlab_execute_graphql_query for queries.");
2352
- }
2353
- return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
2354
- }
3291
+ handler: async (args, context) => listWorkItems(context, resolveProjectId(args, context, true), {
3292
+ types: getOptionalStringArray(args, "types"),
3293
+ state: getOptionalString(args, "state"),
3294
+ search: getOptionalString(args, "search"),
3295
+ assigneeUsernames: getOptionalStringArray(args, "assignee_usernames"),
3296
+ labelNames: getOptionalStringArray(args, "label_names"),
3297
+ first: getOptionalNumber(args, "first"),
3298
+ after: getOptionalString(args, "after")
3299
+ })
2355
3300
  },
2356
3301
  {
2357
- name: "gitlab_execute_graphql",
2358
- title: "Execute GraphQL (Compat)",
2359
- description: "Backward-compatible GraphQL executor. Mutation payloads still honor read-only policy.",
2360
- capabilities: readGraphqlCapabilities,
3302
+ name: "gitlab_create_work_item",
3303
+ title: "Create Work Item",
3304
+ description: "Create a work item of type issue, task, incident, test_case, epic, key_result, objective, requirement, or ticket.",
3305
+ capabilities: writeGraphqlCapabilities,
2361
3306
  inputSchema: {
2362
- query: z.string().min(1),
2363
- variables: optionalRecord
2364
- },
3307
+ project_id: optionalProjectIdSchema,
3308
+ title: z.string().min(1),
3309
+ type: optionalWorkItemType,
3310
+ description: optionalString,
3311
+ labels: optionalStringArray,
3312
+ assignee_usernames: optionalStringArray,
3313
+ parent_iid: workItemIidSchema.optional(),
3314
+ weight: optionalCoercedNumber,
3315
+ health_status: z.enum(["onTrack", "needsAttention", "atRisk"]).optional(),
3316
+ start_date: optionalString,
3317
+ due_date: optionalString,
3318
+ milestone_id: optionalString,
3319
+ iteration_id: optionalString,
3320
+ confidential: optionalCoercedBoolean
3321
+ },
3322
+ handler: async (args, context) => createWorkItem(context, resolveProjectId(args, context, true), {
3323
+ title: getString(args, "title"),
3324
+ type: getOptionalString(args, "type"),
3325
+ description: getOptionalString(args, "description"),
3326
+ labels: getOptionalStringArray(args, "labels"),
3327
+ assigneeUsernames: getOptionalStringArray(args, "assignee_usernames"),
3328
+ parentIid: getOptionalNumber(args, "parent_iid"),
3329
+ weight: getOptionalNumber(args, "weight"),
3330
+ healthStatus: getOptionalString(args, "health_status"),
3331
+ startDate: getOptionalString(args, "start_date"),
3332
+ dueDate: getOptionalString(args, "due_date"),
3333
+ milestoneId: getOptionalString(args, "milestone_id"),
3334
+ iterationId: getOptionalString(args, "iteration_id"),
3335
+ confidential: getOptionalBoolean(args, "confidential")
3336
+ })
3337
+ },
3338
+ {
3339
+ name: "gitlab_update_work_item",
3340
+ title: "Update Work Item",
3341
+ description: "Update a work item title, description, labels, assignees, state, status, hierarchy, linked items, custom fields, dates, milestone, iteration, and incident metadata.",
3342
+ capabilities: writeGraphqlCapabilities,
3343
+ inputSchema: {
3344
+ project_id: optionalProjectIdSchema,
3345
+ iid: workItemIidSchema,
3346
+ title: optionalString,
3347
+ description: optionalString,
3348
+ add_labels: optionalStringArray,
3349
+ remove_labels: optionalStringArray,
3350
+ assignee_usernames: optionalStringArray,
3351
+ state_event: z.enum(["close", "reopen"]).optional(),
3352
+ weight: optionalNumber,
3353
+ status: optionalString,
3354
+ parent_iid: workItemIidSchema.optional(),
3355
+ parent_project_id: optionalProjectIdSchema,
3356
+ remove_parent: optionalCoercedBoolean,
3357
+ children_to_add: nullableOptional(z.array(workItemReferenceSchema)),
3358
+ children_to_remove: nullableOptional(z.array(workItemReferenceSchema)),
3359
+ health_status: z.enum(["onTrack", "needsAttention", "atRisk"]).optional(),
3360
+ start_date: optionalString,
3361
+ due_date: optionalString,
3362
+ milestone_id: optionalString,
3363
+ iteration_id: optionalString,
3364
+ confidential: optionalCoercedBoolean,
3365
+ linked_items_to_add: nullableOptional(z.array(linkedWorkItemReferenceSchema)),
3366
+ linked_items_to_remove: nullableOptional(z.array(workItemReferenceSchema)),
3367
+ custom_fields: nullableOptional(z.array(customFieldValueSchema)),
3368
+ severity: z.enum(["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional(),
3369
+ escalation_status: z.enum(["TRIGGERED", "ACKNOWLEDGED", "RESOLVED", "IGNORED"]).optional()
3370
+ },
3371
+ handler: async (args, context) => updateWorkItem(context, resolveProjectId(args, context, true), getNumber(args, "iid"), {
3372
+ title: getOptionalString(args, "title"),
3373
+ description: getOptionalString(args, "description"),
3374
+ addLabels: getOptionalStringArray(args, "add_labels"),
3375
+ removeLabels: getOptionalStringArray(args, "remove_labels"),
3376
+ assigneeUsernames: getOptionalStringArray(args, "assignee_usernames"),
3377
+ stateEvent: getOptionalString(args, "state_event"),
3378
+ weight: getOptionalNumber(args, "weight"),
3379
+ status: getOptionalString(args, "status"),
3380
+ parentIid: getOptionalNumber(args, "parent_iid"),
3381
+ parentProjectId: getOptionalString(args, "parent_project_id"),
3382
+ removeParent: getOptionalBoolean(args, "remove_parent"),
3383
+ childrenToAdd: getWorkItemReferences(args, "children_to_add", context),
3384
+ childrenToRemove: getWorkItemReferences(args, "children_to_remove", context),
3385
+ healthStatus: getOptionalString(args, "health_status"),
3386
+ startDate: getOptionalString(args, "start_date"),
3387
+ dueDate: getOptionalString(args, "due_date"),
3388
+ milestoneId: getOptionalString(args, "milestone_id"),
3389
+ iterationId: getOptionalString(args, "iteration_id"),
3390
+ confidential: getOptionalBoolean(args, "confidential"),
3391
+ linkedItemsToAdd: getLinkedWorkItemReferences(args, "linked_items_to_add", context),
3392
+ linkedItemsToRemove: getWorkItemReferences(args, "linked_items_to_remove", context),
3393
+ customFields: getOptionalArray(args, "custom_fields"),
3394
+ severity: getOptionalString(args, "severity"),
3395
+ escalationStatus: getOptionalString(args, "escalation_status")
3396
+ })
3397
+ },
3398
+ {
3399
+ name: "gitlab_convert_work_item_type",
3400
+ title: "Convert Work Item Type",
3401
+ description: "Convert a work item to a different type.",
3402
+ capabilities: writeGraphqlCapabilities,
3403
+ inputSchema: {
3404
+ project_id: optionalProjectIdSchema,
3405
+ iid: workItemIidSchema,
3406
+ new_type: workItemTypeSchema
3407
+ },
3408
+ handler: async (args, context) => convertWorkItemType(context, resolveProjectId(args, context, true), getNumber(args, "iid"), getString(args, "new_type"))
3409
+ },
3410
+ {
3411
+ name: "gitlab_list_work_item_statuses",
3412
+ title: "List Work Item Statuses",
3413
+ description: "List available statuses and allowed hierarchy/conversion types for a work item type.",
3414
+ capabilities: readGraphqlCapabilities,
3415
+ inputSchema: {
3416
+ project_id: optionalProjectIdSchema,
3417
+ work_item_type: optionalWorkItemType
3418
+ },
3419
+ handler: async (args, context) => listWorkItemStatuses(context, resolveProjectId(args, context, true), getOptionalString(args, "work_item_type") ?? "issue")
3420
+ },
3421
+ {
3422
+ name: "gitlab_list_custom_field_definitions",
3423
+ title: "List Custom Field Definitions",
3424
+ description: "List custom field definitions for a work item type, including field IDs, types, options, and supported work item types.",
3425
+ capabilities: readGraphqlCapabilities,
3426
+ inputSchema: {
3427
+ project_id: optionalProjectIdSchema,
3428
+ work_item_type: optionalWorkItemType
3429
+ },
3430
+ handler: async (args, context) => listCustomFieldDefinitions(context, resolveProjectId(args, context, true), getOptionalString(args, "work_item_type") ?? "issue")
3431
+ },
3432
+ {
3433
+ name: "gitlab_move_work_item",
3434
+ title: "Move Work Item",
3435
+ description: "Move a work item to a different project.",
3436
+ capabilities: writeGraphqlCapabilities,
3437
+ inputSchema: {
3438
+ project_id: optionalProjectIdSchema,
3439
+ iid: workItemIidSchema,
3440
+ target_project_id: projectIdSchema
3441
+ },
3442
+ handler: async (args, context) => moveWorkItem(context, resolveProjectId(args, context, true), getNumber(args, "iid"), resolveExplicitProjectId(context, getString(args, "target_project_id")))
3443
+ },
3444
+ {
3445
+ name: "gitlab_list_work_item_notes",
3446
+ title: "List Work Item Notes",
3447
+ description: "List threaded discussions and notes on a work item.",
3448
+ capabilities: readGraphqlCapabilities,
3449
+ inputSchema: {
3450
+ project_id: optionalProjectIdSchema,
3451
+ iid: workItemIidSchema,
3452
+ page_size: z.coerce.number().int().positive().max(100).optional(),
3453
+ after: optionalString,
3454
+ sort: z.enum(["CREATED_ASC", "CREATED_DESC"]).optional()
3455
+ },
3456
+ handler: async (args, context) => listWorkItemNotes(context, resolveProjectId(args, context, true), getNumber(args, "iid"), {
3457
+ pageSize: getOptionalNumber(args, "page_size"),
3458
+ after: getOptionalString(args, "after"),
3459
+ sort: getOptionalString(args, "sort")
3460
+ })
3461
+ },
3462
+ {
3463
+ name: "gitlab_create_work_item_note",
3464
+ title: "Create Work Item Note",
3465
+ description: "Add a note or threaded reply to a work item.",
3466
+ capabilities: writeGraphqlCapabilities,
3467
+ inputSchema: {
3468
+ project_id: optionalProjectIdSchema,
3469
+ iid: workItemIidSchema,
3470
+ body: bodySchema,
3471
+ internal: optionalCoercedBoolean,
3472
+ discussion_id: optionalString
3473
+ },
3474
+ handler: async (args, context) => createWorkItemNote(context, resolveProjectId(args, context, true), getNumber(args, "iid"), getString(args, "body"), {
3475
+ internal: getOptionalBoolean(args, "internal"),
3476
+ discussionId: getOptionalString(args, "discussion_id")
3477
+ })
3478
+ },
3479
+ {
3480
+ name: "gitlab_list_work_item_emoji_reactions",
3481
+ title: "List Work Item Emoji Reactions",
3482
+ description: "List emoji reactions on a work item.",
3483
+ capabilities: readGraphqlCapabilities,
3484
+ inputSchema: {
3485
+ project_id: optionalProjectIdSchema,
3486
+ iid: workItemIidSchema
3487
+ },
3488
+ handler: async (args, context) => listGraphqlAwardEmoji(context, (await resolveWorkItemGid(context, resolveProjectId(args, context, true), getNumber(args, "iid"))).workItemGid)
3489
+ },
3490
+ {
3491
+ name: "gitlab_list_work_item_note_emoji_reactions",
3492
+ title: "List Work Item Note Emoji Reactions",
3493
+ description: "List emoji reactions on a work item note by GraphQL note_id.",
3494
+ capabilities: readGraphqlCapabilities,
3495
+ inputSchema: {
3496
+ project_id: optionalProjectIdSchema,
3497
+ iid: workItemIidSchema,
3498
+ note_id: z.string().min(1)
3499
+ },
3500
+ handler: async (args, context) => {
3501
+ const projectId = resolveProjectId(args, context, true);
3502
+ const noteId = await resolveWorkItemNoteAwardableId(context, projectId, getNumber(args, "iid"), getString(args, "note_id"));
3503
+ return listGraphqlAwardEmoji(context, noteId);
3504
+ }
3505
+ },
3506
+ {
3507
+ name: "gitlab_create_work_item_emoji_reaction",
3508
+ title: "Create Work Item Emoji Reaction",
3509
+ description: "Add an emoji reaction to a work item, for example thumbsup, rocket, or eyes.",
3510
+ capabilities: writeGraphqlCapabilities,
3511
+ inputSchema: {
3512
+ project_id: optionalProjectIdSchema,
3513
+ iid: workItemIidSchema,
3514
+ name: emojiNameSchema
3515
+ },
3516
+ handler: async (args, context) => addGraphqlAwardEmoji(context, (await resolveWorkItemGid(context, resolveProjectId(args, context, true), getNumber(args, "iid"))).workItemGid, getString(args, "name"))
3517
+ },
3518
+ {
3519
+ name: "gitlab_delete_work_item_emoji_reaction",
3520
+ title: "Delete Work Item Emoji Reaction",
3521
+ description: "Remove the current user's emoji reaction from a work item by emoji name. Requires iid and name.",
3522
+ capabilities: writeGraphqlCapabilities,
3523
+ inputSchema: {
3524
+ project_id: optionalProjectIdSchema,
3525
+ iid: workItemIidSchema,
3526
+ name: emojiNameSchema
3527
+ },
3528
+ handler: async (args, context) => removeGraphqlAwardEmoji(context, (await resolveWorkItemGid(context, resolveProjectId(args, context, true), getNumber(args, "iid"))).workItemGid, getString(args, "name"))
3529
+ },
3530
+ {
3531
+ name: "gitlab_create_work_item_note_emoji_reaction",
3532
+ title: "Create Work Item Note Emoji Reaction",
3533
+ description: "Add an emoji reaction to a work item note by GraphQL note_id.",
3534
+ capabilities: writeGraphqlCapabilities,
3535
+ inputSchema: {
3536
+ project_id: optionalProjectIdSchema,
3537
+ iid: workItemIidSchema,
3538
+ note_id: z.string().min(1),
3539
+ name: emojiNameSchema
3540
+ },
3541
+ handler: async (args, context) => {
3542
+ const projectId = resolveProjectId(args, context, true);
3543
+ const noteId = await resolveWorkItemNoteAwardableId(context, projectId, getNumber(args, "iid"), getString(args, "note_id"));
3544
+ return addGraphqlAwardEmoji(context, noteId, getString(args, "name"));
3545
+ }
3546
+ },
3547
+ {
3548
+ name: "gitlab_delete_work_item_note_emoji_reaction",
3549
+ title: "Delete Work Item Note Emoji Reaction",
3550
+ description: "Remove the current user's emoji reaction from a work item note by GraphQL note_id and emoji name.",
3551
+ capabilities: writeGraphqlCapabilities,
3552
+ inputSchema: {
3553
+ project_id: optionalProjectIdSchema,
3554
+ iid: workItemIidSchema,
3555
+ note_id: z.string().min(1),
3556
+ name: emojiNameSchema
3557
+ },
3558
+ handler: async (args, context) => {
3559
+ const projectId = resolveProjectId(args, context, true);
3560
+ const noteId = await resolveWorkItemNoteAwardableId(context, projectId, getNumber(args, "iid"), getString(args, "note_id"));
3561
+ return removeGraphqlAwardEmoji(context, noteId, getString(args, "name"));
3562
+ }
3563
+ },
3564
+ {
3565
+ name: "gitlab_get_timeline_events",
3566
+ title: "Get Timeline Events",
3567
+ description: "List timeline events for an incident work item.",
3568
+ capabilities: readGraphqlCapabilities,
3569
+ inputSchema: {
3570
+ project_id: optionalProjectIdSchema,
3571
+ incident_iid: workItemIidSchema
3572
+ },
3573
+ handler: async (args, context) => getTimelineEvents(context, resolveProjectId(args, context, true), getNumber(args, "incident_iid"))
3574
+ },
3575
+ {
3576
+ name: "gitlab_create_timeline_event",
3577
+ title: "Create Timeline Event",
3578
+ description: "Create an incident timeline event with optional known GitLab incident timeline tags.",
3579
+ capabilities: writeGraphqlCapabilities,
3580
+ inputSchema: {
3581
+ project_id: optionalProjectIdSchema,
3582
+ incident_iid: workItemIidSchema,
3583
+ note: bodySchema,
3584
+ occurred_at: z.string().min(1),
3585
+ tag_names: nullableOptional(z.array(z.enum([
3586
+ "Start time",
3587
+ "End time",
3588
+ "Impact detected",
3589
+ "Response initiated",
3590
+ "Impact mitigated",
3591
+ "Cause identified"
3592
+ ])))
3593
+ },
3594
+ handler: async (args, context) => createTimelineEvent(context, resolveProjectId(args, context, true), getNumber(args, "incident_iid"), getString(args, "note"), getString(args, "occurred_at"), getOptionalStringArray(args, "tag_names"))
3595
+ },
3596
+ {
3597
+ name: "gitlab_execute_graphql_query",
3598
+ title: "Execute GraphQL Query",
3599
+ description: "Execute read-only GraphQL query.",
3600
+ capabilities: readGraphqlCapabilities,
3601
+ inputSchema: {
3602
+ query: z.string().min(1),
3603
+ variables: optionalRecord
3604
+ },
3605
+ handler: async (args, context) => {
3606
+ const query = getString(args, "query");
3607
+ if (containsGraphqlMutation(query)) {
3608
+ throw new Error("Mutation detected. Use gitlab_execute_graphql_mutation for mutation operations.");
3609
+ }
3610
+ return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
3611
+ }
3612
+ },
3613
+ {
3614
+ name: "gitlab_execute_graphql_mutation",
3615
+ title: "Execute GraphQL Mutation",
3616
+ description: "Execute GraphQL mutation (disabled in read-only mode).",
3617
+ capabilities: writeGraphqlCapabilities,
3618
+ inputSchema: {
3619
+ query: z.string().min(1),
3620
+ variables: optionalRecord
3621
+ },
3622
+ handler: async (args, context) => {
3623
+ const query = getString(args, "query");
3624
+ if (!containsGraphqlMutation(query)) {
3625
+ throw new Error("No mutation detected. Use gitlab_execute_graphql_query for queries.");
3626
+ }
3627
+ return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
3628
+ }
3629
+ },
3630
+ {
3631
+ name: "gitlab_execute_graphql",
3632
+ title: "Execute GraphQL (Compat)",
3633
+ description: "Backward-compatible GraphQL executor. Mutation payloads still honor read-only policy.",
3634
+ capabilities: readGraphqlCapabilities,
3635
+ inputSchema: {
3636
+ query: z.string().min(1),
3637
+ variables: optionalRecord
3638
+ },
2365
3639
  handler: async (args, context) => {
2366
3640
  const query = getString(args, "query");
2367
3641
  if (containsGraphqlMutation(query)) {
@@ -2375,12 +3649,65 @@ function getGitLabToolDefinitions() {
2375
3649
  }
2376
3650
  ];
2377
3651
  }
3652
+ function shouldReturnDownloadProxy(context) {
3653
+ return !context.allowLocalFileTools && resolveDownloadTokenAuth(context) !== undefined;
3654
+ }
3655
+ function buildDownloadProxyResult(context, resource, filename) {
3656
+ return {
3657
+ download_url: buildDownloadProxyUrl(context, resource),
3658
+ filename,
3659
+ expires_in_seconds: context.env.GITLAB_DOWNLOAD_TOKEN_TTL_SECONDS
3660
+ };
3661
+ }
3662
+ function buildDownloadProxyUrl(context, resource) {
3663
+ const baseUrl = new URL(context.env.MCP_SERVER_URL ?? `http://${context.env.HTTP_HOST}:${String(context.env.HTTP_PORT)}`);
3664
+ const basePath = baseUrl.pathname.replace(/\/+$/, "");
3665
+ const url = new URL(`${basePath}/downloads/${encodeURIComponent(resource.type)}`, baseUrl.origin);
3666
+ for (const [key, value] of Object.entries(resource.params)) {
3667
+ url.searchParams.set(key, value);
3668
+ }
3669
+ const tokenAuth = resolveDownloadTokenAuth(context);
3670
+ if (tokenAuth) {
3671
+ url.searchParams.set("_token", createDownloadToken(tokenAuth, resource, {
3672
+ secret: context.env.GITLAB_DOWNLOAD_TOKEN_SECRET,
3673
+ ttlSeconds: context.env.GITLAB_DOWNLOAD_TOKEN_TTL_SECONDS
3674
+ }));
3675
+ }
3676
+ return url.toString();
3677
+ }
3678
+ function resolveDownloadTokenAuth(context) {
3679
+ const sessionAuth = getSessionAuth();
3680
+ if (sessionAuth?.token) {
3681
+ return {
3682
+ header: sessionAuth.header ?? "private-token",
3683
+ token: sessionAuth.token,
3684
+ apiUrl: context.env.ENABLE_DYNAMIC_API_URL && sessionAuth.apiUrl !== context.env.GITLAB_API_URL
3685
+ ? sessionAuth.apiUrl
3686
+ : undefined
3687
+ };
3688
+ }
3689
+ if (context.env.GITLAB_PERSONAL_ACCESS_TOKEN) {
3690
+ return {
3691
+ header: "private-token",
3692
+ token: context.env.GITLAB_PERSONAL_ACCESS_TOKEN
3693
+ };
3694
+ }
3695
+ if (context.env.GITLAB_JOB_TOKEN) {
3696
+ return {
3697
+ header: "job-token",
3698
+ token: context.env.GITLAB_JOB_TOKEN
3699
+ };
3700
+ }
3701
+ return undefined;
3702
+ }
2378
3703
  function assertAuthReady(context) {
2379
3704
  const auth = getSessionAuth();
2380
- if (context.env.REMOTE_AUTHORIZATION) {
3705
+ if (context.env.REMOTE_AUTHORIZATION || context.env.GITLAB_MCP_OAUTH) {
2381
3706
  const token = auth?.token;
2382
3707
  if (!token) {
2383
- throw new Error("Missing remote authorization token for this session");
3708
+ throw new Error(context.env.REMOTE_AUTHORIZATION
3709
+ ? "Missing remote authorization token for this session"
3710
+ : "Missing OAuth authorization token for this session");
2384
3711
  }
2385
3712
  if (context.env.ENABLE_DYNAMIC_API_URL && !auth?.apiUrl) {
2386
3713
  throw new Error("Missing remote API URL for this session");
@@ -2388,14 +3715,1032 @@ function assertAuthReady(context) {
2388
3715
  return;
2389
3716
  }
2390
3717
  const hasFallbackAuth = Boolean(context.env.GITLAB_PERSONAL_ACCESS_TOKEN) ||
3718
+ Boolean(context.env.GITLAB_JOB_TOKEN) ||
2391
3719
  Boolean(context.env.GITLAB_USE_OAUTH && context.env.GITLAB_OAUTH_CLIENT_ID) ||
2392
3720
  Boolean(context.env.GITLAB_TOKEN_SCRIPT) ||
2393
3721
  Boolean(context.env.GITLAB_TOKEN_FILE) ||
2394
3722
  Boolean(context.env.GITLAB_AUTH_COOKIE_PATH);
2395
3723
  if (!hasFallbackAuth) {
2396
- throw new Error("Authentication required: set GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_TOKEN_SCRIPT, GITLAB_TOKEN_FILE, or GITLAB_AUTH_COOKIE_PATH");
3724
+ throw new Error("Authentication required: set GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_JOB_TOKEN, GITLAB_TOKEN_SCRIPT, GITLAB_TOKEN_FILE, or GITLAB_AUTH_COOKIE_PATH");
3725
+ }
3726
+ }
3727
+ const WORK_ITEM_TYPE_NAMES = {
3728
+ issue: "Issue",
3729
+ task: "Task",
3730
+ incident: "Incident",
3731
+ test_case: "Test Case",
3732
+ epic: "Epic",
3733
+ key_result: "Key Result",
3734
+ objective: "Objective",
3735
+ requirement: "Requirement",
3736
+ ticket: "Ticket"
3737
+ };
3738
+ const WORK_ITEM_GRAPHQL_TYPES = {
3739
+ issue: "ISSUE",
3740
+ task: "TASK",
3741
+ incident: "INCIDENT",
3742
+ test_case: "TEST_CASE",
3743
+ epic: "EPIC",
3744
+ key_result: "KEY_RESULT",
3745
+ objective: "OBJECTIVE",
3746
+ requirement: "REQUIREMENT",
3747
+ ticket: "TICKET"
3748
+ };
3749
+ async function executeGraphqlData(context, query, variables = {}) {
3750
+ const response = (await context.gitlab.executeGraphql(query, variables));
3751
+ if (typeof response === "object" &&
3752
+ response !== null &&
3753
+ "errors" in response &&
3754
+ Array.isArray(response.errors)) {
3755
+ const errors = response.errors;
3756
+ throw new Error(`GraphQL errors: ${errors.map((item) => item.message ?? String(item)).join(", ")}`);
3757
+ }
3758
+ if (typeof response === "object" && response !== null && "data" in response) {
3759
+ return response.data;
3760
+ }
3761
+ return response;
3762
+ }
3763
+ function resolveExplicitProjectId(context, projectId) {
3764
+ const allowed = context.env.GITLAB_ALLOWED_PROJECT_IDS;
3765
+ if (allowed.length > 0 && !allowed.includes(projectId)) {
3766
+ throw new Error(`Project '${projectId}' is not in GITLAB_ALLOWED_PROJECT_IDS: ${allowed.join(", ")}`);
3767
+ }
3768
+ return projectId;
3769
+ }
3770
+ async function resolveProjectPathForWorkItem(context, projectId) {
3771
+ const project = (await context.gitlab.getProject(projectId));
3772
+ const pathWithNamespace = project.path_with_namespace;
3773
+ if (typeof pathWithNamespace === "string" && pathWithNamespace.length > 0) {
3774
+ return pathWithNamespace;
3775
+ }
3776
+ if (projectId.includes("/")) {
3777
+ return projectId;
3778
+ }
3779
+ throw new Error(`Project '${projectId}' did not include path_with_namespace`);
3780
+ }
3781
+ async function resolveWorkItemGid(context, projectId, iid) {
3782
+ const projectPath = await resolveProjectPathForWorkItem(context, projectId);
3783
+ const data = await executeGraphqlData(context, `query($path: ID!, $iid: String!) {
3784
+ namespace(fullPath: $path) {
3785
+ workItem(iid: $iid) { id }
3786
+ }
3787
+ }`, { path: projectPath, iid: String(iid) });
3788
+ const workItemGid = data.namespace?.workItem?.id;
3789
+ if (!workItemGid) {
3790
+ throw new Error(`Work item #${iid} not found in project ${projectPath}`);
3791
+ }
3792
+ return { workItemGid, projectPath };
3793
+ }
3794
+ async function resolveWorkItemTypeGid(context, projectPath, type) {
3795
+ const targetName = WORK_ITEM_TYPE_NAMES[type];
3796
+ const data = await executeGraphqlData(context, `query($path: ID!) {
3797
+ namespace(fullPath: $path) {
3798
+ workItemTypes { nodes { id name } }
3799
+ }
3800
+ }`, { path: projectPath });
3801
+ const match = data.namespace?.workItemTypes?.nodes?.find((item) => item.name === targetName);
3802
+ if (!match) {
3803
+ throw new Error(`Work item type '${targetName}' not found in project ${projectPath}`);
3804
+ }
3805
+ return match.id;
3806
+ }
3807
+ async function resolveNamesToIds(context, projectPath, labelNames, usernames) {
3808
+ if ((!labelNames || labelNames.length === 0) && (!usernames || usernames.length === 0)) {
3809
+ return { labelIds: [], userIds: [] };
3810
+ }
3811
+ const data = await executeGraphqlData(context, `query($path: ID!, $usernames: [String!]!) {
3812
+ project(fullPath: $path) {
3813
+ labels(includeAncestorGroups: true, first: 250) { nodes { id title } }
3814
+ }
3815
+ users(usernames: $usernames) { nodes { id username } }
3816
+ }`, { path: projectPath, usernames: usernames ?? [] });
3817
+ const labels = data.project?.labels?.nodes ?? [];
3818
+ const users = data.users?.nodes ?? [];
3819
+ const labelIds = (labelNames ?? []).map((name) => {
3820
+ const label = labels.find((item) => item.title === name);
3821
+ if (!label) {
3822
+ throw new Error(`Label '${name}' not found in project ${projectPath}`);
3823
+ }
3824
+ return label.id;
3825
+ });
3826
+ const userIds = (usernames ?? []).map((username) => {
3827
+ const user = users.find((item) => item.username === username);
3828
+ if (!user) {
3829
+ throw new Error(`User '${username}' not found`);
3830
+ }
3831
+ return user.id;
3832
+ });
3833
+ return { labelIds, userIds };
3834
+ }
3835
+ function normalizeGlobalId(value, typeName) {
3836
+ return value.startsWith("gid://") ? value : `gid://gitlab/${typeName}/${value}`;
3837
+ }
3838
+ function toWorkItemGraphqlType(type) {
3839
+ return WORK_ITEM_GRAPHQL_TYPES[type] ?? type.replace(/ /g, "_").toUpperCase();
3840
+ }
3841
+ function workItemTypeName(type) {
3842
+ return WORK_ITEM_TYPE_NAMES[type] ?? "Issue";
3843
+ }
3844
+ function findWidget(widgets, typename) {
3845
+ if (!Array.isArray(widgets)) {
3846
+ return undefined;
3847
+ }
3848
+ return widgets.find((item) => typeof item === "object" && item !== null && item.__typename === typename);
3849
+ }
3850
+ async function getWorkItem(context, projectId, iid) {
3851
+ const projectPath = await resolveProjectPathForWorkItem(context, projectId);
3852
+ const data = await executeGraphqlData(context, `query($path: ID!, $iid: String!) {
3853
+ namespace(fullPath: $path) {
3854
+ workItem(iid: $iid) {
3855
+ id
3856
+ iid
3857
+ title
3858
+ state
3859
+ description
3860
+ webUrl
3861
+ confidential
3862
+ author { username }
3863
+ createdAt
3864
+ closedAt
3865
+ workItemType { name }
3866
+ widgets {
3867
+ __typename
3868
+ ... on WorkItemWidgetHierarchy {
3869
+ hasChildren
3870
+ hasParent
3871
+ parent { id iid title webUrl workItemType { name } namespace { fullPath } }
3872
+ children { nodes { id iid title state webUrl workItemType { name } namespace { fullPath } } }
3873
+ }
3874
+ ... on WorkItemWidgetStatus { status { id name category color iconName position } }
3875
+ ... on WorkItemWidgetCustomFields {
3876
+ customFieldValues {
3877
+ __typename
3878
+ customField { id name fieldType }
3879
+ ... on WorkItemNumberFieldValue { value }
3880
+ ... on WorkItemTextFieldValue { value }
3881
+ ... on WorkItemSelectFieldValue { selectedOptions { id value } }
3882
+ }
3883
+ }
3884
+ ... on WorkItemWidgetLabels { labels { nodes { id title color } } }
3885
+ ... on WorkItemWidgetAssignees { assignees { nodes { id username name } } }
3886
+ ... on WorkItemWidgetWeight { weight rolledUpWeight rolledUpCompletedWeight }
3887
+ ... on WorkItemWidgetHealthStatus { healthStatus }
3888
+ ... on WorkItemWidgetStartAndDueDate { startDate dueDate }
3889
+ ... on WorkItemWidgetMilestone { milestone { id title } }
3890
+ ... on WorkItemWidgetLinkedItems {
3891
+ blocked
3892
+ blockedByCount
3893
+ blockingCount
3894
+ linkedItems { nodes { linkType workItem { id iid title state webUrl workItemType { name } namespace { fullPath } } } }
3895
+ }
3896
+ ... on WorkItemWidgetTimeTracking { timeEstimate totalTimeSpent }
3897
+ ... on WorkItemWidgetDevelopment {
3898
+ willAutoCloseByMergeRequest
3899
+ relatedBranches { nodes { name } }
3900
+ relatedMergeRequests { nodes { iid title webUrl state sourceBranch } }
3901
+ closingMergeRequests { nodes { mergeRequest { iid title webUrl state sourceBranch } } }
3902
+ featureFlags { nodes { name active } }
3903
+ }
3904
+ ... on WorkItemWidgetIteration {
3905
+ iteration { id title startDate dueDate webUrl iterationCadence { id title } }
3906
+ }
3907
+ ... on WorkItemWidgetProgress { progress }
3908
+ ... on WorkItemWidgetColor { color textColor }
3909
+ }
3910
+ }
3911
+ }
3912
+ }`, { path: projectPath, iid: String(iid) });
3913
+ const workItem = data.namespace?.workItem;
3914
+ if (!workItem) {
3915
+ throw new Error(`Work item #${iid} not found in project ${projectPath}`);
3916
+ }
3917
+ return flattenWorkItem(workItem);
3918
+ }
3919
+ function flattenWorkItem(workItem) {
3920
+ const widgets = workItem.widgets ?? [];
3921
+ const hierarchy = findWidget(widgets, "WorkItemWidgetHierarchy");
3922
+ const status = findWidget(widgets, "WorkItemWidgetStatus");
3923
+ const labels = findWidget(widgets, "WorkItemWidgetLabels");
3924
+ const assignees = findWidget(widgets, "WorkItemWidgetAssignees");
3925
+ const weight = findWidget(widgets, "WorkItemWidgetWeight");
3926
+ const health = findWidget(widgets, "WorkItemWidgetHealthStatus");
3927
+ const dates = findWidget(widgets, "WorkItemWidgetStartAndDueDate");
3928
+ const milestone = findWidget(widgets, "WorkItemWidgetMilestone");
3929
+ const linked = findWidget(widgets, "WorkItemWidgetLinkedItems");
3930
+ const timeTracking = findWidget(widgets, "WorkItemWidgetTimeTracking");
3931
+ const development = findWidget(widgets, "WorkItemWidgetDevelopment");
3932
+ const customFields = findWidget(widgets, "WorkItemWidgetCustomFields");
3933
+ const iteration = findWidget(widgets, "WorkItemWidgetIteration");
3934
+ const progress = findWidget(widgets, "WorkItemWidgetProgress");
3935
+ const color = findWidget(widgets, "WorkItemWidgetColor");
3936
+ const result = {
3937
+ id: workItem.id,
3938
+ iid: workItem.iid,
3939
+ title: workItem.title,
3940
+ state: workItem.state,
3941
+ type: workItem.workItemType?.name,
3942
+ webUrl: workItem.webUrl
3943
+ };
3944
+ if (workItem.description)
3945
+ result.description = workItem.description;
3946
+ if (workItem.confidential)
3947
+ result.confidential = true;
3948
+ if (workItem.author?.username)
3949
+ result.author = workItem.author.username;
3950
+ if (workItem.createdAt)
3951
+ result.createdAt = workItem.createdAt;
3952
+ if (workItem.closedAt)
3953
+ result.closedAt = workItem.closedAt;
3954
+ if (status?.status) {
3955
+ result.status = {
3956
+ id: status.status.id,
3957
+ name: status.status.name,
3958
+ category: status.status.category
3959
+ };
3960
+ }
3961
+ const labelNames = (labels?.labels?.nodes ?? []).map((item) => item.title);
3962
+ if (labelNames.length > 0)
3963
+ result.labels = labelNames;
3964
+ const assigneeNames = (assignees?.assignees?.nodes ?? []).map((item) => item.username);
3965
+ if (assigneeNames.length > 0)
3966
+ result.assignees = assigneeNames;
3967
+ if (weight?.weight != null) {
3968
+ result.weight = weight.weight;
3969
+ if (weight.rolledUpWeight != null)
3970
+ result.rolledUpWeight = weight.rolledUpWeight;
3971
+ if (weight.rolledUpCompletedWeight != null) {
3972
+ result.rolledUpCompletedWeight = weight.rolledUpCompletedWeight;
3973
+ }
3974
+ }
3975
+ if (health?.healthStatus)
3976
+ result.healthStatus = health.healthStatus;
3977
+ if (dates?.startDate)
3978
+ result.startDate = dates.startDate;
3979
+ if (dates?.dueDate)
3980
+ result.dueDate = dates.dueDate;
3981
+ if (milestone?.milestone)
3982
+ result.milestone = milestone.milestone;
3983
+ if (iteration?.iteration)
3984
+ result.iteration = iteration.iteration;
3985
+ if (progress?.progress != null)
3986
+ result.progress = progress.progress;
3987
+ if (color?.color)
3988
+ result.color = color.color;
3989
+ if (hierarchy?.parent) {
3990
+ result.parent = {
3991
+ iid: hierarchy.parent.iid,
3992
+ title: hierarchy.parent.title,
3993
+ type: hierarchy.parent.workItemType?.name,
3994
+ project: hierarchy.parent.namespace?.fullPath,
3995
+ webUrl: hierarchy.parent.webUrl
3996
+ };
3997
+ }
3998
+ const children = hierarchy?.children?.nodes ?? [];
3999
+ if (children.length > 0) {
4000
+ result.children = children.map((item) => ({
4001
+ iid: item.iid,
4002
+ title: item.title,
4003
+ state: item.state,
4004
+ type: item.workItemType?.name,
4005
+ project: item.namespace?.fullPath,
4006
+ webUrl: item.webUrl
4007
+ }));
4008
+ }
4009
+ if (linked?.blocked)
4010
+ result.blocked = true;
4011
+ if ((linked?.blockedByCount ?? 0) > 0)
4012
+ result.blockedByCount = linked?.blockedByCount;
4013
+ if ((linked?.blockingCount ?? 0) > 0)
4014
+ result.blockingCount = linked?.blockingCount;
4015
+ const linkedItems = linked?.linkedItems?.nodes ?? [];
4016
+ if (linkedItems.length > 0) {
4017
+ result.linkedItems = linkedItems.map((item) => ({
4018
+ linkType: item.linkType,
4019
+ iid: item.workItem?.iid,
4020
+ title: item.workItem?.title,
4021
+ state: item.workItem?.state,
4022
+ type: item.workItem?.workItemType?.name,
4023
+ project: item.workItem?.namespace?.fullPath,
4024
+ webUrl: item.workItem?.webUrl
4025
+ }));
4026
+ }
4027
+ if ((timeTracking?.timeEstimate ?? 0) > 0)
4028
+ result.timeEstimate = timeTracking?.timeEstimate;
4029
+ if ((timeTracking?.totalTimeSpent ?? 0) > 0) {
4030
+ result.totalTimeSpent = timeTracking?.totalTimeSpent;
4031
+ }
4032
+ const relatedMergeRequests = development?.relatedMergeRequests?.nodes ?? [];
4033
+ const closingMergeRequests = (development?.closingMergeRequests?.nodes ?? []).map((item) => item.mergeRequest);
4034
+ const branches = development?.relatedBranches?.nodes ?? [];
4035
+ const flags = development?.featureFlags?.nodes ?? [];
4036
+ if (relatedMergeRequests.length > 0 ||
4037
+ closingMergeRequests.length > 0 ||
4038
+ branches.length > 0 ||
4039
+ flags.length > 0) {
4040
+ result.development = {};
4041
+ if (relatedMergeRequests.length > 0) {
4042
+ result.development.relatedMergeRequests = relatedMergeRequests;
4043
+ }
4044
+ if (closingMergeRequests.length > 0) {
4045
+ result.development.closingMergeRequests = closingMergeRequests;
4046
+ }
4047
+ if (branches.length > 0) {
4048
+ result.development.relatedBranches = branches.map((item) => item.name);
4049
+ }
4050
+ if (flags.length > 0)
4051
+ result.development.featureFlags = flags;
4052
+ }
4053
+ const fieldValues = (customFields?.customFieldValues ?? []).filter((item) => item.value != null || item.selectedOptions != null);
4054
+ if (fieldValues.length > 0) {
4055
+ result.customFields = fieldValues.map((item) => ({
4056
+ name: item.customField?.name,
4057
+ type: item.customField?.fieldType,
4058
+ value: item.value ?? item.selectedOptions ?? null
4059
+ }));
4060
+ }
4061
+ return result;
4062
+ }
4063
+ async function listWorkItems(context, projectId, options) {
4064
+ const projectPath = await resolveProjectPathForWorkItem(context, projectId);
4065
+ const variables = {
4066
+ path: projectPath,
4067
+ first: options.first ?? 20,
4068
+ types: options.types?.map(toWorkItemGraphqlType),
4069
+ state: options.state,
4070
+ search: options.search,
4071
+ assigneeUsernames: options.assigneeUsernames,
4072
+ labelName: options.labelNames,
4073
+ after: options.after
4074
+ };
4075
+ const data = await executeGraphqlData(context, `query($path: ID!, $types: [IssueType!], $state: IssuableState, $search: String, $assigneeUsernames: [String!], $labelName: [String!], $first: Int, $after: String) {
4076
+ project(fullPath: $path) {
4077
+ workItems(types: $types, state: $state, search: $search, assigneeUsernames: $assigneeUsernames, labelName: $labelName, first: $first, after: $after) {
4078
+ nodes {
4079
+ id iid title state webUrl workItemType { name }
4080
+ widgets {
4081
+ __typename
4082
+ ... on WorkItemWidgetStatus { status { id name category color } }
4083
+ ... on WorkItemWidgetLabels { labels { nodes { title } } }
4084
+ ... on WorkItemWidgetAssignees { assignees { nodes { username } } }
4085
+ ... on WorkItemWidgetWeight { weight }
4086
+ ... on WorkItemWidgetHealthStatus { healthStatus }
4087
+ ... on WorkItemWidgetStartAndDueDate { startDate dueDate }
4088
+ ... on WorkItemWidgetMilestone { milestone { id title } }
4089
+ }
4090
+ }
4091
+ pageInfo { hasNextPage endCursor }
4092
+ }
4093
+ }
4094
+ }`, variables);
4095
+ const nodes = data.project?.workItems?.nodes ?? [];
4096
+ return {
4097
+ items: nodes.map(flattenWorkItemSummary),
4098
+ pageInfo: data.project?.workItems?.pageInfo ?? {}
4099
+ };
4100
+ }
4101
+ function flattenWorkItemSummary(workItem) {
4102
+ const widgets = workItem.widgets ?? [];
4103
+ const status = findWidget(widgets, "WorkItemWidgetStatus");
4104
+ const labels = findWidget(widgets, "WorkItemWidgetLabels");
4105
+ const assignees = findWidget(widgets, "WorkItemWidgetAssignees");
4106
+ const weight = findWidget(widgets, "WorkItemWidgetWeight");
4107
+ const health = findWidget(widgets, "WorkItemWidgetHealthStatus");
4108
+ const dates = findWidget(widgets, "WorkItemWidgetStartAndDueDate");
4109
+ const milestone = findWidget(widgets, "WorkItemWidgetMilestone");
4110
+ const item = {
4111
+ iid: workItem.iid,
4112
+ title: workItem.title,
4113
+ state: workItem.state,
4114
+ type: workItem.workItemType?.name,
4115
+ webUrl: workItem.webUrl
4116
+ };
4117
+ if (status?.status)
4118
+ item.status = status.status.name;
4119
+ const labelNames = (labels?.labels?.nodes ?? []).map((label) => label.title);
4120
+ if (labelNames.length > 0)
4121
+ item.labels = labelNames;
4122
+ const assigneeNames = (assignees?.assignees?.nodes ?? []).map((assignee) => assignee.username);
4123
+ if (assigneeNames.length > 0)
4124
+ item.assignees = assigneeNames;
4125
+ if (weight?.weight != null)
4126
+ item.weight = weight.weight;
4127
+ if (health?.healthStatus)
4128
+ item.healthStatus = health.healthStatus;
4129
+ if (dates?.startDate)
4130
+ item.startDate = dates.startDate;
4131
+ if (dates?.dueDate)
4132
+ item.dueDate = dates.dueDate;
4133
+ if (milestone?.milestone)
4134
+ item.milestone = milestone.milestone.title;
4135
+ return item;
4136
+ }
4137
+ async function createWorkItem(context, projectId, options) {
4138
+ const projectPath = await resolveProjectPathForWorkItem(context, projectId);
4139
+ const typeId = await resolveWorkItemTypeGid(context, projectPath, options.type ?? "issue");
4140
+ const variableDefinitions = ["$projectPath: ID!", "$title: String!", "$typeId: WorkItemsTypeID!"];
4141
+ const inputParts = ["namespacePath: $projectPath", "title: $title", "workItemTypeId: $typeId"];
4142
+ const variables = {
4143
+ projectPath,
4144
+ title: options.title,
4145
+ typeId
4146
+ };
4147
+ if (options.description !== undefined) {
4148
+ variableDefinitions.push("$description: String!");
4149
+ inputParts.push("descriptionWidget: { description: $description }");
4150
+ variables.description = options.description;
4151
+ }
4152
+ const { labelIds, userIds } = await resolveNamesToIds(context, projectPath, options.labels, options.assigneeUsernames);
4153
+ if (labelIds.length > 0) {
4154
+ variableDefinitions.push("$labelIds: [LabelID!]!");
4155
+ inputParts.push("labelsWidget: { labelIds: $labelIds }");
4156
+ variables.labelIds = labelIds;
4157
+ }
4158
+ if (userIds.length > 0) {
4159
+ variableDefinitions.push("$assigneeIds: [UserID!]!");
4160
+ inputParts.push("assigneesWidget: { assigneeIds: $assigneeIds }");
4161
+ variables.assigneeIds = userIds;
4162
+ }
4163
+ if (options.weight !== undefined) {
4164
+ variableDefinitions.push("$weight: Int");
4165
+ inputParts.push("weightWidget: { weight: $weight }");
4166
+ variables.weight = options.weight;
4167
+ }
4168
+ if (options.parentIid !== undefined) {
4169
+ const { workItemGid: parentId } = await resolveWorkItemGid(context, projectId, options.parentIid);
4170
+ variableDefinitions.push("$parentId: WorkItemID");
4171
+ inputParts.push("hierarchyWidget: { parentId: $parentId }");
4172
+ variables.parentId = parentId;
4173
+ }
4174
+ if (options.healthStatus !== undefined) {
4175
+ variableDefinitions.push("$healthStatus: HealthStatus");
4176
+ inputParts.push("healthStatusWidget: { healthStatus: $healthStatus }");
4177
+ variables.healthStatus = options.healthStatus;
4178
+ }
4179
+ appendDateWidget(variableDefinitions, inputParts, variables, options.startDate, options.dueDate);
4180
+ if (options.milestoneId !== undefined) {
4181
+ variableDefinitions.push("$milestoneId: MilestoneID");
4182
+ inputParts.push("milestoneWidget: { milestoneId: $milestoneId }");
4183
+ variables.milestoneId = normalizeGlobalId(options.milestoneId, "Milestone");
4184
+ }
4185
+ if (options.iterationId !== undefined) {
4186
+ variableDefinitions.push("$iterationId: IterationID");
4187
+ inputParts.push("iterationWidget: { iterationId: $iterationId }");
4188
+ variables.iterationId = normalizeGlobalId(options.iterationId, "Iteration");
4189
+ }
4190
+ if (options.confidential !== undefined) {
4191
+ variableDefinitions.push("$confidential: Boolean");
4192
+ inputParts.push("confidential: $confidential");
4193
+ variables.confidential = options.confidential;
4194
+ }
4195
+ const data = await executeGraphqlData(context, `mutation(${variableDefinitions.join(", ")}) {
4196
+ workItemCreate(input: { ${inputParts.join(", ")} }) {
4197
+ workItem { id iid title webUrl workItemType { name } }
4198
+ errors
4199
+ }
4200
+ }`, variables);
4201
+ assertNoGraphqlMutationErrors(data.workItemCreate?.errors, "Failed to create work item");
4202
+ const workItem = data.workItemCreate.workItem;
4203
+ return {
4204
+ id: workItem?.id,
4205
+ iid: workItem?.iid,
4206
+ title: workItem?.title,
4207
+ type: workItem?.workItemType?.name,
4208
+ webUrl: workItem?.webUrl
4209
+ };
4210
+ }
4211
+ function appendDateWidget(variableDefinitions, inputParts, variables, startDate, dueDate) {
4212
+ if (startDate === undefined && dueDate === undefined) {
4213
+ return;
4214
+ }
4215
+ const dateParts = [];
4216
+ if (startDate !== undefined) {
4217
+ variableDefinitions.push("$startDate: Date");
4218
+ dateParts.push("startDate: $startDate");
4219
+ variables.startDate = startDate;
4220
+ }
4221
+ if (dueDate !== undefined) {
4222
+ variableDefinitions.push("$dueDate: Date");
4223
+ dateParts.push("dueDate: $dueDate");
4224
+ variables.dueDate = dueDate;
4225
+ }
4226
+ inputParts.push(`startAndDueDateWidget: { ${dateParts.join(", ")} }`);
4227
+ }
4228
+ async function updateWorkItem(context, projectId, iid, options) {
4229
+ const { workItemGid, projectPath } = await resolveWorkItemGid(context, projectId, iid);
4230
+ const variableDefinitions = ["$id: WorkItemID!"];
4231
+ const inputParts = ["id: $id"];
4232
+ const variables = { id: workItemGid };
4233
+ if (options.title !== undefined) {
4234
+ variableDefinitions.push("$title: String");
4235
+ inputParts.push("title: $title");
4236
+ variables.title = options.title;
4237
+ }
4238
+ if (options.description !== undefined) {
4239
+ variableDefinitions.push("$description: String!");
4240
+ inputParts.push("descriptionWidget: { description: $description }");
4241
+ variables.description = options.description;
4242
+ }
4243
+ const allLabelNames = [...(options.addLabels ?? []), ...(options.removeLabels ?? [])];
4244
+ const needsNameResolution = allLabelNames.length > 0 || !!options.assigneeUsernames?.length;
4245
+ const { labelIds, userIds } = needsNameResolution
4246
+ ? await resolveNamesToIds(context, projectPath, allLabelNames.length > 0 ? allLabelNames : undefined, options.assigneeUsernames)
4247
+ : { labelIds: [], userIds: [] };
4248
+ if (options.addLabels || options.removeLabels) {
4249
+ const labelParts = [];
4250
+ if (options.addLabels && options.addLabels.length > 0) {
4251
+ variableDefinitions.push("$addLabelIds: [LabelID!]");
4252
+ labelParts.push("addLabelIds: $addLabelIds");
4253
+ variables.addLabelIds = labelIds.slice(0, options.addLabels.length);
4254
+ }
4255
+ if (options.removeLabels && options.removeLabels.length > 0) {
4256
+ variableDefinitions.push("$removeLabelIds: [LabelID!]");
4257
+ labelParts.push("removeLabelIds: $removeLabelIds");
4258
+ variables.removeLabelIds = labelIds.slice(options.addLabels?.length ?? 0);
4259
+ }
4260
+ if (labelParts.length > 0)
4261
+ inputParts.push(`labelsWidget: { ${labelParts.join(", ")} }`);
4262
+ }
4263
+ if (userIds.length > 0) {
4264
+ variableDefinitions.push("$assigneeIds: [UserID!]!");
4265
+ inputParts.push("assigneesWidget: { assigneeIds: $assigneeIds }");
4266
+ variables.assigneeIds = userIds;
4267
+ }
4268
+ if (options.stateEvent !== undefined) {
4269
+ variableDefinitions.push("$stateEvent: WorkItemStateEvent");
4270
+ inputParts.push("stateEvent: $stateEvent");
4271
+ variables.stateEvent = options.stateEvent === "close" ? "CLOSE" : "REOPEN";
4272
+ }
4273
+ if (options.weight !== undefined) {
4274
+ variableDefinitions.push("$weight: Int");
4275
+ inputParts.push("weightWidget: { weight: $weight }");
4276
+ variables.weight = options.weight;
2397
4277
  }
4278
+ if (options.status !== undefined) {
4279
+ variableDefinitions.push("$status: WorkItemsStatusesStatusID");
4280
+ inputParts.push("statusWidget: { status: $status }");
4281
+ variables.status = options.status;
4282
+ }
4283
+ if (options.healthStatus !== undefined) {
4284
+ variableDefinitions.push("$healthStatus: HealthStatus");
4285
+ inputParts.push("healthStatusWidget: { healthStatus: $healthStatus }");
4286
+ variables.healthStatus = options.healthStatus;
4287
+ }
4288
+ appendDateWidget(variableDefinitions, inputParts, variables, options.startDate, options.dueDate);
4289
+ if (options.milestoneId !== undefined) {
4290
+ variableDefinitions.push("$milestoneId: MilestoneID");
4291
+ inputParts.push("milestoneWidget: { milestoneId: $milestoneId }");
4292
+ variables.milestoneId = normalizeGlobalId(options.milestoneId, "Milestone");
4293
+ }
4294
+ if (options.iterationId !== undefined) {
4295
+ variableDefinitions.push("$iterationId: IterationID");
4296
+ inputParts.push("iterationWidget: { iterationId: $iterationId }");
4297
+ variables.iterationId = normalizeGlobalId(options.iterationId, "Iteration");
4298
+ }
4299
+ if (options.confidential !== undefined) {
4300
+ variableDefinitions.push("$confidential: Boolean");
4301
+ inputParts.push("confidential: $confidential");
4302
+ variables.confidential = options.confidential;
4303
+ }
4304
+ if (options.customFields && options.customFields.length > 0) {
4305
+ variableDefinitions.push("$customFieldsWidget: [WorkItemWidgetCustomFieldValueInputType!]");
4306
+ inputParts.push("customFieldsWidget: $customFieldsWidget");
4307
+ variables.customFieldsWidget = options.customFields.map((field) => ({
4308
+ customFieldId: normalizeGlobalId(field.custom_field_id, "IssuablesCustomField"),
4309
+ textValue: field.text_value,
4310
+ numberValue: field.number_value,
4311
+ selectedOptionIds: field.selected_option_ids,
4312
+ dateValue: field.date_value
4313
+ }));
4314
+ }
4315
+ if (options.removeParent) {
4316
+ inputParts.push("hierarchyWidget: { parentId: null }");
4317
+ }
4318
+ else if (options.parentIid !== undefined) {
4319
+ const parentProjectId = options.parentProjectId ?? projectId;
4320
+ const { workItemGid: parentId } = await resolveWorkItemGid(context, parentProjectId, options.parentIid);
4321
+ variableDefinitions.push("$parentId: WorkItemID");
4322
+ inputParts.push("hierarchyWidget: { parentId: $parentId }");
4323
+ variables.parentId = parentId;
4324
+ }
4325
+ const data = await executeGraphqlData(context, `mutation(${variableDefinitions.join(", ")}) {
4326
+ workItemUpdate(input: { ${inputParts.join(", ")} }) {
4327
+ workItem {
4328
+ id iid title state webUrl workItemType { name }
4329
+ widgets {
4330
+ __typename
4331
+ ... on WorkItemWidgetStatus { status { id name category color } }
4332
+ ... on WorkItemWidgetLabels { labels { nodes { title } } }
4333
+ ... on WorkItemWidgetAssignees { assignees { nodes { username } } }
4334
+ ... on WorkItemWidgetWeight { weight }
4335
+ ... on WorkItemWidgetHierarchy { parent { id title workItemType { name } } }
4336
+ ... on WorkItemWidgetHealthStatus { healthStatus }
4337
+ ... on WorkItemWidgetStartAndDueDate { startDate dueDate }
4338
+ ... on WorkItemWidgetMilestone { milestone { id title } }
4339
+ }
4340
+ }
4341
+ errors
4342
+ }
4343
+ }`, variables);
4344
+ assertNoGraphqlMutationErrors(data.workItemUpdate?.errors, "Failed to update work item");
4345
+ await updateWorkItemRelationships(context, workItemGid, options);
4346
+ if (options.severity !== undefined) {
4347
+ await updateIncidentSeverity(context, projectPath, iid, options.severity);
4348
+ }
4349
+ if (options.escalationStatus !== undefined) {
4350
+ await updateIncidentEscalationStatus(context, projectPath, iid, options.escalationStatus);
4351
+ }
4352
+ const workItem = data.workItemUpdate.workItem ?? {};
4353
+ return {
4354
+ ...flattenWorkItemSummary(workItem),
4355
+ id: workItem.id,
4356
+ children_added: options.childrenToAdd?.length ?? 0,
4357
+ children_removed: options.childrenToRemove?.length ?? 0,
4358
+ linked_items_added: options.linkedItemsToAdd?.length ?? 0,
4359
+ linked_items_removed: options.linkedItemsToRemove?.length ?? 0,
4360
+ ...(options.severity !== undefined ? { severity: options.severity } : {}),
4361
+ ...(options.escalationStatus !== undefined
4362
+ ? { escalation_status: options.escalationStatus }
4363
+ : {})
4364
+ };
4365
+ }
4366
+ async function updateWorkItemRelationships(context, workItemGid, options) {
4367
+ if (options.childrenToAdd && options.childrenToAdd.length > 0) {
4368
+ const childIds = [];
4369
+ for (const child of options.childrenToAdd) {
4370
+ const { workItemGid: childId } = await resolveWorkItemGid(context, child.project_id, child.iid);
4371
+ childIds.push(childId);
4372
+ }
4373
+ const data = await executeGraphqlData(context, `mutation($id: WorkItemID!, $childrenIds: [WorkItemID!]!) {
4374
+ workItemUpdate(input: { id: $id, hierarchyWidget: { childrenIds: $childrenIds } }) {
4375
+ errors
4376
+ }
4377
+ }`, { id: workItemGid, childrenIds: childIds });
4378
+ assertNoGraphqlMutationErrors(data.workItemUpdate?.errors, "Failed to add children");
4379
+ }
4380
+ if (options.childrenToRemove) {
4381
+ for (const child of options.childrenToRemove) {
4382
+ await removeWorkItemParent(context, child.project_id, child.iid);
4383
+ }
4384
+ }
4385
+ if (options.linkedItemsToAdd && options.linkedItemsToAdd.length > 0) {
4386
+ const grouped = {};
4387
+ for (const item of options.linkedItemsToAdd) {
4388
+ const linkType = item.link_type ?? "RELATED";
4389
+ const { workItemGid: targetId } = await resolveWorkItemGid(context, item.project_id, item.iid);
4390
+ grouped[linkType] = [...(grouped[linkType] ?? []), targetId];
4391
+ }
4392
+ for (const [linkType, targetIds] of Object.entries(grouped)) {
4393
+ const data = await executeGraphqlData(context, `mutation($id: WorkItemID!, $workItemsIds: [WorkItemID!]!, $linkType: WorkItemRelatedLinkType!) {
4394
+ workItemAddLinkedItems(input: { id: $id, workItemsIds: $workItemsIds, linkType: $linkType }) {
4395
+ errors
4396
+ }
4397
+ }`, { id: workItemGid, workItemsIds: targetIds, linkType });
4398
+ assertNoGraphqlMutationErrors(data.workItemAddLinkedItems?.errors, "Failed to add linked items");
4399
+ }
4400
+ }
4401
+ if (options.linkedItemsToRemove && options.linkedItemsToRemove.length > 0) {
4402
+ const targetIds = [];
4403
+ for (const item of options.linkedItemsToRemove) {
4404
+ const { workItemGid: targetId } = await resolveWorkItemGid(context, item.project_id, item.iid);
4405
+ targetIds.push(targetId);
4406
+ }
4407
+ const data = await executeGraphqlData(context, `mutation($id: WorkItemID!, $workItemsIds: [WorkItemID!]!) {
4408
+ workItemRemoveLinkedItems(input: { id: $id, workItemsIds: $workItemsIds }) { errors }
4409
+ }`, { id: workItemGid, workItemsIds: targetIds });
4410
+ assertNoGraphqlMutationErrors(data.workItemRemoveLinkedItems?.errors, "Failed to remove linked items");
4411
+ }
4412
+ }
4413
+ async function removeWorkItemParent(context, projectId, iid) {
4414
+ const { workItemGid } = await resolveWorkItemGid(context, projectId, iid);
4415
+ const data = await executeGraphqlData(context, `mutation($id: WorkItemID!) {
4416
+ workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: null } }) { errors }
4417
+ }`, { id: workItemGid });
4418
+ assertNoGraphqlMutationErrors(data.workItemUpdate?.errors, "Failed to remove parent");
4419
+ }
4420
+ async function convertWorkItemType(context, projectId, iid, newType) {
4421
+ const { workItemGid, projectPath } = await resolveWorkItemGid(context, projectId, iid);
4422
+ const typeId = await resolveWorkItemTypeGid(context, projectPath, newType);
4423
+ const data = await executeGraphqlData(context, `mutation($id: WorkItemID!, $typeId: WorkItemsTypeID!) {
4424
+ workItemConvert(input: { id: $id, workItemTypeId: $typeId }) {
4425
+ workItem { id workItemType { name } }
4426
+ errors
4427
+ }
4428
+ }`, { id: workItemGid, typeId });
4429
+ assertNoGraphqlMutationErrors(data.workItemConvert?.errors, "Conversion failed");
4430
+ return {
4431
+ id: data.workItemConvert.workItem?.id,
4432
+ type: data.workItemConvert.workItem?.workItemType?.name
4433
+ };
2398
4434
  }
4435
+ async function listWorkItemStatuses(context, projectId, type) {
4436
+ const projectPath = await resolveProjectPathForWorkItem(context, projectId);
4437
+ const typeName = workItemTypeName(type);
4438
+ const data = await executeGraphqlData(context, `query($path: ID!, $typeName: IssueType) {
4439
+ namespace(fullPath: $path) {
4440
+ workItemTypes(name: $typeName) {
4441
+ nodes {
4442
+ id
4443
+ name
4444
+ supportedConversionTypes { id name }
4445
+ widgetDefinitions {
4446
+ __typename
4447
+ ... on WorkItemWidgetDefinitionStatus {
4448
+ allowedStatuses { id name iconName color position }
4449
+ }
4450
+ ... on WorkItemWidgetDefinitionHierarchy {
4451
+ allowedChildTypes { nodes { id name } }
4452
+ allowedParentTypes { nodes { id name } }
4453
+ }
4454
+ }
4455
+ }
4456
+ }
4457
+ }
4458
+ }`, { path: projectPath, typeName: typeName.replace(/ /g, "_").toUpperCase() });
4459
+ const typeNode = data.namespace?.workItemTypes?.nodes?.[0];
4460
+ if (!typeNode) {
4461
+ throw new Error(`Work item type '${typeName}' not found in project ${projectPath}`);
4462
+ }
4463
+ const statusWidget = typeNode.widgetDefinitions?.find((widget) => widget.__typename === "WorkItemWidgetDefinitionStatus");
4464
+ const hierarchyWidget = typeNode.widgetDefinitions?.find((widget) => widget.__typename === "WorkItemWidgetDefinitionHierarchy");
4465
+ return {
4466
+ work_item_type: typeNode.name,
4467
+ statuses_available: (statusWidget?.allowedStatuses ?? []).length > 0,
4468
+ statuses: statusWidget?.allowedStatuses ?? [],
4469
+ supported_conversion_types: (typeNode.supportedConversionTypes ?? []).map((item) => item.name),
4470
+ allowed_child_types: (hierarchyWidget?.allowedChildTypes?.nodes ?? []).map((item) => item.name),
4471
+ allowed_parent_types: (hierarchyWidget?.allowedParentTypes?.nodes ?? []).map((item) => item.name)
4472
+ };
4473
+ }
4474
+ async function listCustomFieldDefinitions(context, projectId, type) {
4475
+ const projectPath = await resolveProjectPathForWorkItem(context, projectId);
4476
+ const typeName = workItemTypeName(type);
4477
+ const data = await executeGraphqlData(context, `query($path: ID!, $typeName: IssueType) {
4478
+ namespace(fullPath: $path) {
4479
+ workItemTypes(name: $typeName) {
4480
+ nodes {
4481
+ id
4482
+ name
4483
+ widgetDefinitions {
4484
+ __typename
4485
+ ... on WorkItemWidgetDefinitionCustomFields {
4486
+ customFieldValues {
4487
+ customField {
4488
+ id
4489
+ name
4490
+ fieldType
4491
+ selectOptions { id value }
4492
+ workItemTypes { id name }
4493
+ }
4494
+ }
4495
+ }
4496
+ }
4497
+ }
4498
+ }
4499
+ }
4500
+ }`, { path: projectPath, typeName: typeName.replace(/ /g, "_").toUpperCase() });
4501
+ const typeNode = data.namespace?.workItemTypes?.nodes?.[0];
4502
+ if (!typeNode) {
4503
+ throw new Error(`Work item type '${typeName}' not found in project ${projectPath}`);
4504
+ }
4505
+ const widget = typeNode.widgetDefinitions?.find((item) => item.__typename === "WorkItemWidgetDefinitionCustomFields");
4506
+ return {
4507
+ work_item_type: typeNode.name,
4508
+ custom_fields: (widget?.customFieldValues ?? []).map((item) => {
4509
+ const field = item.customField ?? {};
4510
+ return {
4511
+ id: field.id,
4512
+ name: field.name,
4513
+ type: field.fieldType,
4514
+ ...(field.selectOptions?.length ? { selectOptions: field.selectOptions } : {}),
4515
+ ...(field.workItemTypes?.length
4516
+ ? { workItemTypes: field.workItemTypes.map((workItemType) => workItemType.name) }
4517
+ : {})
4518
+ };
4519
+ })
4520
+ };
4521
+ }
4522
+ async function moveWorkItem(context, projectId, iid, targetProjectId) {
4523
+ const projectPath = await resolveProjectPathForWorkItem(context, projectId);
4524
+ const targetProjectPath = await resolveProjectPathForWorkItem(context, targetProjectId);
4525
+ const data = await executeGraphqlData(context, `mutation($projectPath: ID!, $iid: String!, $targetProjectPath: ID!) {
4526
+ issueMove(input: { projectPath: $projectPath, iid: $iid, targetProjectPath: $targetProjectPath }) {
4527
+ issue { id iid webUrl }
4528
+ errors
4529
+ }
4530
+ }`, { projectPath, iid: String(iid), targetProjectPath });
4531
+ assertNoGraphqlMutationErrors(data.issueMove?.errors, "Failed to move work item");
4532
+ return data.issueMove.issue;
4533
+ }
4534
+ async function listWorkItemNotes(context, projectId, iid, options) {
4535
+ const projectPath = await resolveProjectPathForWorkItem(context, projectId);
4536
+ const data = await executeGraphqlData(context, `query($path: ID!, $iid: String!, $pageSize: Int, $after: String, $sort: WorkItemDiscussionsSort) {
4537
+ namespace(fullPath: $path) {
4538
+ workItem(iid: $iid) {
4539
+ id
4540
+ widgets(onlyTypes: [NOTES]) {
4541
+ ... on WorkItemWidgetNotes {
4542
+ discussionLocked
4543
+ discussions(first: $pageSize, after: $after, filter: ALL_NOTES, sort: $sort) {
4544
+ pageInfo { hasNextPage endCursor }
4545
+ nodes {
4546
+ id
4547
+ resolved
4548
+ resolvable
4549
+ notes {
4550
+ nodes {
4551
+ id
4552
+ body
4553
+ system
4554
+ internal
4555
+ createdAt
4556
+ lastEditedAt
4557
+ author { username }
4558
+ }
4559
+ }
4560
+ }
4561
+ }
4562
+ }
4563
+ }
4564
+ }
4565
+ }
4566
+ }`, {
4567
+ path: projectPath,
4568
+ iid: String(iid),
4569
+ pageSize: options.pageSize ?? 20,
4570
+ after: options.after ?? null,
4571
+ sort: options.sort ?? "CREATED_ASC"
4572
+ });
4573
+ const workItem = data.namespace?.workItem;
4574
+ if (!workItem) {
4575
+ throw new Error(`Work item #${iid} not found in project ${projectPath}`);
4576
+ }
4577
+ const notesWidget = (workItem.widgets ?? []).find((widget) => widget.discussions);
4578
+ const discussions = notesWidget?.discussions;
4579
+ return {
4580
+ discussions: (discussions?.nodes ?? []).map((discussion) => ({
4581
+ id: discussion.id,
4582
+ resolved: discussion.resolved,
4583
+ resolvable: discussion.resolvable,
4584
+ notes: (discussion.notes?.nodes ?? []).map((note) => ({
4585
+ id: note.id,
4586
+ author: note.author?.username,
4587
+ body: note.body,
4588
+ createdAt: note.createdAt,
4589
+ ...(note.system ? { system: true } : {}),
4590
+ ...(note.internal ? { internal: true } : {}),
4591
+ ...(note.lastEditedAt ? { lastEditedAt: note.lastEditedAt } : {})
4592
+ }))
4593
+ })),
4594
+ pageInfo: discussions?.pageInfo ?? {}
4595
+ };
4596
+ }
4597
+ async function resolveWorkItemNoteAwardableId(context, projectId, iid, noteId) {
4598
+ let after;
4599
+ for (let page = 0; page < 100; page += 1) {
4600
+ const result = (await listWorkItemNotes(context, projectId, iid, {
4601
+ pageSize: 100,
4602
+ after,
4603
+ sort: "CREATED_ASC"
4604
+ }));
4605
+ const found = (result.discussions ?? []).some((discussion) => (discussion.notes ?? []).some((note) => note.id === noteId));
4606
+ if (found) {
4607
+ return noteId;
4608
+ }
4609
+ if (result.pageInfo?.hasNextPage !== true || typeof result.pageInfo.endCursor !== "string") {
4610
+ break;
4611
+ }
4612
+ after = result.pageInfo.endCursor;
4613
+ }
4614
+ throw new Error(`Note '${noteId}' was not found on work item #${iid} in project ${projectId}`);
4615
+ }
4616
+ async function createWorkItemNote(context, projectId, iid, body, options) {
4617
+ const { workItemGid } = await resolveWorkItemGid(context, projectId, iid);
4618
+ const variableDefinitions = ["$noteableId: NoteableID!", "$body: String!"];
4619
+ const inputParts = ["noteableId: $noteableId", "body: $body"];
4620
+ const variables = { noteableId: workItemGid, body };
4621
+ if (options.internal) {
4622
+ variableDefinitions.push("$internal: Boolean");
4623
+ inputParts.push("internal: $internal");
4624
+ variables.internal = true;
4625
+ }
4626
+ if (options.discussionId) {
4627
+ variableDefinitions.push("$discussionId: DiscussionID");
4628
+ inputParts.push("discussionId: $discussionId");
4629
+ variables.discussionId = options.discussionId;
4630
+ }
4631
+ const data = await executeGraphqlData(context, `mutation(${variableDefinitions.join(", ")}) {
4632
+ createNote(input: { ${inputParts.join(", ")} }) {
4633
+ note { id body discussion { id } }
4634
+ errors
4635
+ }
4636
+ }`, variables);
4637
+ assertNoGraphqlMutationErrors(data.createNote?.errors, "Failed to create note");
4638
+ return data.createNote.note;
4639
+ }
4640
+ async function addGraphqlAwardEmoji(context, awardableId, name) {
4641
+ const data = await executeGraphqlData(context, `mutation($awardableId: AwardableID!, $name: String!) {
4642
+ awardEmojiAdd(input: { awardableId: $awardableId, name: $name }) {
4643
+ awardEmoji { name user { username } }
4644
+ errors
4645
+ }
4646
+ }`, { awardableId, name });
4647
+ assertNoGraphqlMutationErrors(data.awardEmojiAdd?.errors, "Failed to add emoji reaction");
4648
+ return data.awardEmojiAdd.awardEmoji;
4649
+ }
4650
+ async function listGraphqlAwardEmoji(context, awardableId) {
4651
+ const data = await executeGraphqlData(context, `query($id: AwardableID!) {
4652
+ awardable(id: $id) {
4653
+ awardEmoji { nodes { name user { username } } }
4654
+ }
4655
+ }`, { id: awardableId });
4656
+ return data.awardable?.awardEmoji?.nodes ?? [];
4657
+ }
4658
+ async function removeGraphqlAwardEmoji(context, awardableId, name) {
4659
+ const data = await executeGraphqlData(context, `mutation($awardableId: AwardableID!, $name: String!) {
4660
+ awardEmojiRemove(input: { awardableId: $awardableId, name: $name }) {
4661
+ awardEmoji { name }
4662
+ errors
4663
+ }
4664
+ }`, { awardableId, name });
4665
+ assertNoGraphqlMutationErrors(data.awardEmojiRemove?.errors, "Failed to remove emoji reaction");
4666
+ return data.awardEmojiRemove.awardEmoji ?? { status: "success" };
4667
+ }
4668
+ async function getTimelineEvents(context, projectId, incidentIid) {
4669
+ const { workItemGid, projectPath } = await resolveWorkItemGid(context, projectId, incidentIid);
4670
+ const incidentId = workItemGid.replace("/WorkItem/", "/Issue/");
4671
+ const data = await executeGraphqlData(context, `query($fullPath: ID!, $incidentId: IssueID!) {
4672
+ project(fullPath: $fullPath) {
4673
+ incidentManagementTimelineEvents(incidentId: $incidentId) {
4674
+ nodes {
4675
+ id
4676
+ note
4677
+ noteHtml
4678
+ action
4679
+ occurredAt
4680
+ createdAt
4681
+ timelineEventTags { nodes { id name } }
4682
+ }
4683
+ }
4684
+ }
4685
+ }`, { fullPath: projectPath, incidentId });
4686
+ return (data.project?.incidentManagementTimelineEvents?.nodes ?? []).map((event) => ({
4687
+ id: event.id,
4688
+ note: event.note,
4689
+ action: event.action,
4690
+ occurredAt: event.occurredAt,
4691
+ createdAt: event.createdAt,
4692
+ ...(event.noteHtml ? { noteHtml: event.noteHtml } : {}),
4693
+ ...(event.timelineEventTags?.nodes?.length
4694
+ ? { tags: event.timelineEventTags.nodes.map((tag) => tag.name) }
4695
+ : {})
4696
+ }));
4697
+ }
4698
+ async function createTimelineEvent(context, projectId, incidentIid, note, occurredAt, tagNames) {
4699
+ const { workItemGid } = await resolveWorkItemGid(context, projectId, incidentIid);
4700
+ const incidentId = workItemGid.replace("/WorkItem/", "/Issue/");
4701
+ const input = { incidentId, note, occurredAt };
4702
+ if (tagNames && tagNames.length > 0) {
4703
+ input.timelineEventTagNames = tagNames;
4704
+ }
4705
+ const data = await executeGraphqlData(context, `mutation CreateTimelineEvent($input: TimelineEventCreateInput!) {
4706
+ timelineEventCreate(input: $input) {
4707
+ timelineEvent {
4708
+ id
4709
+ note
4710
+ noteHtml
4711
+ action
4712
+ occurredAt
4713
+ createdAt
4714
+ timelineEventTags { nodes { id name } }
4715
+ }
4716
+ errors
4717
+ }
4718
+ }`, { input });
4719
+ assertNoGraphqlMutationErrors(data.timelineEventCreate?.errors, "Failed to create timeline event");
4720
+ return data.timelineEventCreate.timelineEvent;
4721
+ }
4722
+ async function updateIncidentSeverity(context, projectPath, incidentIid, severity) {
4723
+ const data = await executeGraphqlData(context, `mutation($projectPath: ID!, $severity: IssuableSeverity!, $iid: String!) {
4724
+ issueSetSeverity(input: { iid: $iid, severity: $severity, projectPath: $projectPath }) {
4725
+ errors
4726
+ }
4727
+ }`, { projectPath, severity, iid: String(incidentIid) });
4728
+ assertNoGraphqlMutationErrors(data.issueSetSeverity?.errors, "Failed to set severity");
4729
+ }
4730
+ async function updateIncidentEscalationStatus(context, projectPath, incidentIid, status) {
4731
+ const data = await executeGraphqlData(context, `mutation($projectPath: ID!, $status: IssueEscalationStatus!, $iid: String!) {
4732
+ issueSetEscalationStatus(input: { projectPath: $projectPath, status: $status, iid: $iid }) {
4733
+ errors
4734
+ }
4735
+ }`, { projectPath, status, iid: String(incidentIid) });
4736
+ assertNoGraphqlMutationErrors(data.issueSetEscalationStatus?.errors, "Failed to set escalation status");
4737
+ }
4738
+ function assertNoGraphqlMutationErrors(errors, prefix) {
4739
+ if (errors && errors.length > 0) {
4740
+ throw new Error(`${prefix}: ${errors.join(", ")}`);
4741
+ }
4742
+ }
4743
+ /* eslint-enable @typescript-eslint/no-explicit-any */
2399
4744
  export function containsGraphqlMutation(query) {
2400
4745
  if (!query.trim()) {
2401
4746
  return false;
@@ -2459,7 +4804,25 @@ export function shouldDisableGraphqlTools(allowedProjectIds, allowGraphqlWithPro
2459
4804
  function isGraphqlToolName(name) {
2460
4805
  return (name === "gitlab_execute_graphql_query" ||
2461
4806
  name === "gitlab_execute_graphql_mutation" ||
2462
- name === "gitlab_execute_graphql");
4807
+ name === "gitlab_execute_graphql" ||
4808
+ name === "gitlab_get_work_item" ||
4809
+ name === "gitlab_list_work_items" ||
4810
+ name === "gitlab_create_work_item" ||
4811
+ name === "gitlab_update_work_item" ||
4812
+ name === "gitlab_convert_work_item_type" ||
4813
+ name === "gitlab_list_work_item_statuses" ||
4814
+ name === "gitlab_list_custom_field_definitions" ||
4815
+ name === "gitlab_move_work_item" ||
4816
+ name === "gitlab_list_work_item_notes" ||
4817
+ name === "gitlab_create_work_item_note" ||
4818
+ name === "gitlab_list_work_item_emoji_reactions" ||
4819
+ name === "gitlab_list_work_item_note_emoji_reactions" ||
4820
+ name === "gitlab_create_work_item_emoji_reaction" ||
4821
+ name === "gitlab_delete_work_item_emoji_reaction" ||
4822
+ name === "gitlab_create_work_item_note_emoji_reaction" ||
4823
+ name === "gitlab_delete_work_item_note_emoji_reaction" ||
4824
+ name === "gitlab_get_timeline_events" ||
4825
+ name === "gitlab_create_timeline_event");
2463
4826
  }
2464
4827
  function resolveProjectId(args, context, required) {
2465
4828
  const fromArgs = getOptionalString(args, "project_id");
@@ -2542,6 +4905,20 @@ function toStructuredContent(value) {
2542
4905
  value
2543
4906
  };
2544
4907
  }
4908
+ function maybeDecodeRepositoryFileContents(value, shouldDecode) {
4909
+ if (!shouldDecode || typeof value !== "object" || value === null || Array.isArray(value)) {
4910
+ return value;
4911
+ }
4912
+ const file = value;
4913
+ if (file.encoding !== "base64" || typeof file.content !== "string") {
4914
+ return value;
4915
+ }
4916
+ return {
4917
+ ...file,
4918
+ content: Buffer.from(file.content, "base64").toString("utf8"),
4919
+ encoding: "utf8"
4920
+ };
4921
+ }
2545
4922
  function omit(args, keys) {
2546
4923
  const result = {};
2547
4924
  for (const [key, value] of Object.entries(args)) {
@@ -2580,6 +4957,88 @@ function toCsvValue(value) {
2580
4957
  }
2581
4958
  return undefined;
2582
4959
  }
4960
+ async function resolveMergeRequestIid(args, context, projectId, options) {
4961
+ const mergeRequestIid = getOptionalString(args, "merge_request_iid");
4962
+ if (mergeRequestIid) {
4963
+ return mergeRequestIid;
4964
+ }
4965
+ const sourceBranch = getOptionalString(args, "source_branch");
4966
+ if (!sourceBranch) {
4967
+ throw new Error("Either merge_request_iid or source_branch must be provided");
4968
+ }
4969
+ const candidates = await context.gitlab.listMergeRequests(projectId, {
4970
+ query: {
4971
+ source_branch: sourceBranch,
4972
+ per_page: 100,
4973
+ page: 1
4974
+ }
4975
+ });
4976
+ const match = pickMergeRequestForSourceBranch(candidates, sourceBranch, options);
4977
+ return getMergeRequestIid(match);
4978
+ }
4979
+ function extractMergeRequestChanges(value) {
4980
+ if (typeof value !== "object" || value === null || !("changes" in value)) {
4981
+ return [];
4982
+ }
4983
+ const changes = value.changes;
4984
+ return extractMergeRequestDiffRecords(changes);
4985
+ }
4986
+ function extractMergeRequestDiffRecords(value) {
4987
+ if (!Array.isArray(value)) {
4988
+ return [];
4989
+ }
4990
+ return value.filter((item) => typeof item === "object" && item !== null);
4991
+ }
4992
+ function filterChangedFiles(files, patterns) {
4993
+ if (!patterns || patterns.length === 0) {
4994
+ return files;
4995
+ }
4996
+ const regexes = patterns.map((pattern) => new RegExp(pattern));
4997
+ return files.filter((file) => {
4998
+ const paths = [file.new_path, file.old_path].filter((value) => typeof value === "string");
4999
+ return !regexes.some((regex) => paths.some((filePath) => regex.test(filePath)));
5000
+ });
5001
+ }
5002
+ function resolveWebhookScope(args) {
5003
+ const projectId = getOptionalString(args, "project_id");
5004
+ const groupId = getOptionalString(args, "group_id");
5005
+ if ((projectId ? 1 : 0) + (groupId ? 1 : 0) !== 1) {
5006
+ throw new Error("Provide exactly one of project_id or group_id");
5007
+ }
5008
+ return projectId ? { projectId } : { groupId };
5009
+ }
5010
+ function summarizeWebhookEvents(events) {
5011
+ return events.map((event) => ({
5012
+ id: event.id,
5013
+ url: event.url,
5014
+ trigger: event.trigger,
5015
+ response_status: event.response_status,
5016
+ execution_duration: event.execution_duration
5017
+ }));
5018
+ }
5019
+ async function findWebhookEvent(context, scope, hookId, eventId, page) {
5020
+ const perPage = 20;
5021
+ const pages = page ? [page] : Array.from({ length: 25 }, (_value, index) => index + 1);
5022
+ for (const currentPage of pages) {
5023
+ const events = extractRecords(await context.gitlab.listWebhookEvents(scope, hookId, {
5024
+ query: { page: currentPage, per_page: perPage }
5025
+ }));
5026
+ const match = events.find((event) => String(event.id) === eventId);
5027
+ if (match) {
5028
+ return match;
5029
+ }
5030
+ if (events.length < perPage) {
5031
+ break;
5032
+ }
5033
+ }
5034
+ return undefined;
5035
+ }
5036
+ function extractRecords(value) {
5037
+ if (!Array.isArray(value)) {
5038
+ return [];
5039
+ }
5040
+ return value.filter((item) => typeof item === "object" && item !== null);
5041
+ }
2583
5042
  function pickMergeRequestForSourceBranch(value, sourceBranch, options) {
2584
5043
  const matches = extractMergeRequestRecords(value).filter((item) => {
2585
5044
  const candidateBranch = item.source_branch;
@@ -2625,6 +5084,122 @@ function getMergeRequestIid(mergeRequest) {
2625
5084
  }
2626
5085
  throw new Error("Matched merge request is missing a valid iid");
2627
5086
  }
5087
+ async function getDetailedMergeRequestFromMatch(projectId, mergeRequest, context) {
5088
+ if (typeof context.gitlab.getMergeRequest !== "function") {
5089
+ return mergeRequest;
5090
+ }
5091
+ return context.gitlab.getMergeRequest(projectId, getMergeRequestIid(mergeRequest));
5092
+ }
5093
+ async function withMergeRequestSummaries(projectId, mergeRequest, context) {
5094
+ if (typeof mergeRequest !== "object" || mergeRequest === null || Array.isArray(mergeRequest)) {
5095
+ return mergeRequest;
5096
+ }
5097
+ const record = mergeRequest;
5098
+ const [commitAdditionSummary, approvalSummary] = await Promise.all([
5099
+ buildMergeRequestCommitAdditionSummary(projectId, record, context),
5100
+ buildMergeRequestApprovalSummary(projectId, record, context)
5101
+ ]);
5102
+ return {
5103
+ ...record,
5104
+ commit_addition_summary: commitAdditionSummary,
5105
+ approval_summary: approvalSummary
5106
+ };
5107
+ }
5108
+ async function buildMergeRequestCommitAdditionSummary(projectId, mergeRequest, context) {
5109
+ const targetBranch = typeof mergeRequest.target_branch === "string" ? mergeRequest.target_branch : null;
5110
+ try {
5111
+ const sourceCommitCount = await context.gitlab.countMergeRequestCommits(projectId, getMergeRequestIid(mergeRequest));
5112
+ const project = (await context.gitlab.getProject(projectId));
5113
+ const mergeMethod = typeof project.merge_method === "string" ? project.merge_method : null;
5114
+ const mergeCommitCount = estimateMergeCommitCount(mergeMethod, sourceCommitCount);
5115
+ const summary = targetBranch && mergeCommitCount !== null
5116
+ ? `${sourceCommitCount} commits and ${mergeCommitCount} merge commit${mergeCommitCount === 1 ? "" : "s"} will be added to ${targetBranch}.`
5117
+ : null;
5118
+ return {
5119
+ target_branch: targetBranch,
5120
+ source_commits_count: sourceCommitCount,
5121
+ merge_method: mergeMethod,
5122
+ merge_commit_count: mergeCommitCount,
5123
+ summary
5124
+ };
5125
+ }
5126
+ catch (error) {
5127
+ return {
5128
+ target_branch: targetBranch,
5129
+ source_commits_count: null,
5130
+ merge_method: null,
5131
+ merge_commit_count: null,
5132
+ summary: null,
5133
+ unavailable_reason: error instanceof Error ? error.message : String(error)
5134
+ };
5135
+ }
5136
+ }
5137
+ function estimateMergeCommitCount(mergeMethod, sourceCommitCount) {
5138
+ if (sourceCommitCount === 0) {
5139
+ return 0;
5140
+ }
5141
+ if (mergeMethod === "merge") {
5142
+ return 1;
5143
+ }
5144
+ if (mergeMethod === "ff" || mergeMethod === "rebase_merge") {
5145
+ return 0;
5146
+ }
5147
+ return null;
5148
+ }
5149
+ async function buildMergeRequestApprovalSummary(projectId, mergeRequest, context) {
5150
+ try {
5151
+ const approvalState = (await context.gitlab.getMergeRequestApprovalState(projectId, getMergeRequestIid(mergeRequest)));
5152
+ const approvedBy = extractRecords(approvalState.approved_by);
5153
+ const approvedByUsernames = getApprovalUsernames(approvalState, approvedBy);
5154
+ const rules = extractRecords(approvalState.rules);
5155
+ return {
5156
+ approved: typeof approvalState.approved === "boolean"
5157
+ ? approvalState.approved
5158
+ : inferMergeRequestApproved(rules),
5159
+ user_has_approved: typeof approvalState.user_has_approved === "boolean"
5160
+ ? approvalState.user_has_approved
5161
+ : null,
5162
+ user_can_approve: typeof approvalState.user_can_approve === "boolean" ? approvalState.user_can_approve : null,
5163
+ approved_by: approvedBy,
5164
+ approved_by_usernames: approvedByUsernames,
5165
+ rules_count: Array.isArray(approvalState.rules) ? approvalState.rules.length : null,
5166
+ source_endpoint: approvalState.source_endpoint === "approval_state" ||
5167
+ approvalState.source_endpoint === "approvals"
5168
+ ? approvalState.source_endpoint
5169
+ : null
5170
+ };
5171
+ }
5172
+ catch (error) {
5173
+ return {
5174
+ approved: null,
5175
+ user_has_approved: null,
5176
+ user_can_approve: null,
5177
+ approved_by: [],
5178
+ approved_by_usernames: [],
5179
+ rules_count: null,
5180
+ source_endpoint: null,
5181
+ unavailable_reason: error instanceof Error ? error.message : String(error)
5182
+ };
5183
+ }
5184
+ }
5185
+ function getApprovalUsernames(approvalState, approvedBy) {
5186
+ const explicit = approvalState.approved_by_usernames;
5187
+ if (Array.isArray(explicit) && explicit.every((item) => typeof item === "string")) {
5188
+ return explicit;
5189
+ }
5190
+ return approvedBy
5191
+ .map((user) => user.username)
5192
+ .filter((username) => typeof username === "string");
5193
+ }
5194
+ function inferMergeRequestApproved(rules) {
5195
+ if (rules.length === 0) {
5196
+ return null;
5197
+ }
5198
+ if (rules.some((rule) => typeof rule.approved !== "boolean")) {
5199
+ return null;
5200
+ }
5201
+ return rules.every((rule) => rule.approved === true);
5202
+ }
2628
5203
  function getString(args, key) {
2629
5204
  const value = args[key];
2630
5205
  if (typeof value !== "string" || value.length === 0) {
@@ -2632,6 +5207,13 @@ function getString(args, key) {
2632
5207
  }
2633
5208
  return value;
2634
5209
  }
5210
+ function getIdString(args, key) {
5211
+ const value = args[key];
5212
+ if ((typeof value !== "string" && typeof value !== "number") || String(value).length === 0) {
5213
+ throw new Error(`'${key}' must be a non-empty string or number`);
5214
+ }
5215
+ return String(value);
5216
+ }
2635
5217
  function getOptionalString(args, key) {
2636
5218
  const value = args[key];
2637
5219
  if (value === undefined) {
@@ -2659,6 +5241,21 @@ function getBoolean(args, key) {
2659
5241
  }
2660
5242
  return value;
2661
5243
  }
5244
+ function getNumber(args, key) {
5245
+ const value = args[key];
5246
+ const numericValue = typeof value === "string" ? Number(value) : value;
5247
+ if (typeof numericValue !== "number" || Number.isNaN(numericValue)) {
5248
+ throw new Error(`'${key}' must be number`);
5249
+ }
5250
+ return numericValue;
5251
+ }
5252
+ function getRequiredStringArray(args, key) {
5253
+ const value = getOptionalStringArray(args, key);
5254
+ if (!value || value.length === 0) {
5255
+ throw new Error(`'${key}' must be a non-empty string array`);
5256
+ }
5257
+ return value;
5258
+ }
2662
5259
  function getOptionalBoolean(args, key) {
2663
5260
  const value = args[key];
2664
5261
  if (value === undefined) {
@@ -2689,6 +5286,47 @@ function getOptionalArray(args, key) {
2689
5286
  }
2690
5287
  return value;
2691
5288
  }
5289
+ function getWorkItemReferences(args, key, context) {
5290
+ const values = getOptionalArray(args, key);
5291
+ if (!values) {
5292
+ return undefined;
5293
+ }
5294
+ return values.map((value) => {
5295
+ if (typeof value !== "object" || value === null) {
5296
+ throw new Error(`'${key}' must contain objects`);
5297
+ }
5298
+ const record = value;
5299
+ if (typeof record.project_id !== "string") {
5300
+ throw new Error(`'${key}.project_id' must be string`);
5301
+ }
5302
+ const iid = typeof record.iid === "string" ? Number(record.iid) : record.iid;
5303
+ if (typeof iid !== "number" || Number.isNaN(iid)) {
5304
+ throw new Error(`'${key}.iid' must be number`);
5305
+ }
5306
+ return {
5307
+ project_id: resolveExplicitProjectId(context, record.project_id),
5308
+ iid
5309
+ };
5310
+ });
5311
+ }
5312
+ function getLinkedWorkItemReferences(args, key, context) {
5313
+ const references = getWorkItemReferences(args, key, context);
5314
+ const rawValues = getOptionalArray(args, key);
5315
+ if (!references || !rawValues) {
5316
+ return references;
5317
+ }
5318
+ return references.map((reference, index) => {
5319
+ const raw = rawValues[index];
5320
+ const linkType = raw.link_type;
5321
+ if (linkType !== undefined && typeof linkType !== "string") {
5322
+ throw new Error(`'${key}.link_type' must be string`);
5323
+ }
5324
+ return {
5325
+ ...reference,
5326
+ link_type: linkType
5327
+ };
5328
+ });
5329
+ }
2692
5330
  function getOptionalNumberArray(args, key) {
2693
5331
  const value = args[key];
2694
5332
  if (value === undefined) {