efficient-gitlab-mcp-server 2.2.0 → 2.4.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 (2) hide show
  1. package/dist/index.js +1412 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -53296,6 +53296,10 @@ var CATEGORIES = [
53296
53296
  name: "webhooks",
53297
53297
  description: "List project webhooks and their recent events."
53298
53298
  },
53299
+ {
53300
+ name: "work-items",
53301
+ description: "Work items via GraphQL: create, update, convert types, manage hierarchy, notes, statuses, custom fields, and incident timeline events."
53302
+ },
53299
53303
  {
53300
53304
  name: "graphql",
53301
53305
  description: "Execute arbitrary GraphQL queries against the GitLab API."
@@ -53632,6 +53636,25 @@ ${errorBody}`);
53632
53636
  }
53633
53637
  return response;
53634
53638
  }
53639
+ async graphql(query, variables = {}) {
53640
+ const idx = this.apiUrl.lastIndexOf("/api/v4");
53641
+ const prefix = idx >= 0 ? this.apiUrl.slice(0, idx) : this.apiUrl;
53642
+ const graphqlUrl = process.env.GITLAB_GRAPHQL_URL || `${prefix}/api/graphql`;
53643
+ const response = await fetch(graphqlUrl, {
53644
+ method: "POST",
53645
+ headers: this.getHeaders(),
53646
+ body: JSON.stringify({ query, variables })
53647
+ });
53648
+ if (!response.ok) {
53649
+ const errorBody = await response.text();
53650
+ throw new Error(`GraphQL request failed (${response.status}): ${errorBody}`);
53651
+ }
53652
+ const json2 = await response.json();
53653
+ if (json2.errors && json2.errors.length > 0) {
53654
+ throw new Error(`GraphQL errors: ${json2.errors.map((e) => e.message).join(", ")}`);
53655
+ }
53656
+ return json2.data;
53657
+ }
53635
53658
  getApiUrl() {
53636
53659
  return this.apiUrl;
53637
53660
  }
@@ -53760,16 +53783,9 @@ function registerGraphqlTools(server, logger2) {
53760
53783
  }
53761
53784
  }, async (params) => {
53762
53785
  const args = ExecuteGraphQLSchema.parse(params);
53763
- const apiUrl = defaultClient.getApiUrl();
53764
- const idx = apiUrl.lastIndexOf("/api/v4");
53765
- const prefix = idx >= 0 ? apiUrl.slice(0, idx) : apiUrl;
53766
- const graphqlUrl = process.env.GITLAB_GRAPHQL_URL || `${prefix}/api/graphql`;
53767
- logger2.info("execute_graphql request", { endpoint: graphqlUrl });
53786
+ logger2.info("execute_graphql request");
53768
53787
  try {
53769
- const result = await defaultClient.post(graphqlUrl, {
53770
- query: args.query,
53771
- variables: args.variables || {}
53772
- });
53788
+ const result = await defaultClient.graphql(args.query, args.variables || {});
53773
53789
  return {
53774
53790
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
53775
53791
  };
@@ -57612,6 +57628,1392 @@ function registerWikiTools(server, logger2) {
57612
57628
  logger2.debug("Wiki tools registered", { count: tools.size });
57613
57629
  return tools;
57614
57630
  }
57631
+ // src/tools/work-items.ts
57632
+ var workItemTypeEnum = exports_external.string().transform((v) => v.toLowerCase()).pipe(exports_external.enum([
57633
+ "issue",
57634
+ "task",
57635
+ "incident",
57636
+ "test_case",
57637
+ "epic",
57638
+ "key_result",
57639
+ "objective",
57640
+ "requirement",
57641
+ "ticket"
57642
+ ]));
57643
+ var WORK_ITEM_TYPE_NAMES = {
57644
+ issue: "Issue",
57645
+ task: "Task",
57646
+ incident: "Incident",
57647
+ test_case: "Test Case",
57648
+ epic: "Epic",
57649
+ key_result: "Key Result",
57650
+ objective: "Objective",
57651
+ requirement: "Requirement",
57652
+ ticket: "Ticket"
57653
+ };
57654
+ var WorkItemParamsSchema = exports_external.object({
57655
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57656
+ iid: exports_external.coerce.number().describe("The internal ID (IID) of the work item")
57657
+ });
57658
+ var GetWorkItemSchema = WorkItemParamsSchema;
57659
+ var ListWorkItemsSchema = exports_external.object({
57660
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57661
+ types: exports_external.array(workItemTypeEnum).optional().describe("Filter by work item types. If not set, returns all types."),
57662
+ state: exports_external.enum(["opened", "closed"]).optional().describe("Filter by state"),
57663
+ search: exports_external.string().optional().describe("Search in title and description"),
57664
+ assignee_usernames: exports_external.array(exports_external.string()).optional().describe("Filter by assignee usernames"),
57665
+ label_names: exports_external.array(exports_external.string()).optional().describe("Filter by label names"),
57666
+ first: exports_external.coerce.number().optional().default(20).describe("Number of items to return (max 100). Default 20."),
57667
+ after: exports_external.string().optional().describe("Cursor for pagination (from previous response's endCursor)")
57668
+ });
57669
+ var CreateWorkItemSchema = exports_external.object({
57670
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57671
+ title: exports_external.string().describe("Title of the work item"),
57672
+ type: workItemTypeEnum.optional().default("issue").describe("Type of work item to create. Defaults to 'issue'."),
57673
+ description: exports_external.string().optional().describe("Description of the work item (Markdown supported)"),
57674
+ labels: exports_external.array(exports_external.string()).optional().describe("Array of label names to assign"),
57675
+ assignee_usernames: exports_external.array(exports_external.string()).optional().describe("Array of usernames to assign"),
57676
+ parent_iid: exports_external.coerce.number().optional().describe("IID of the parent work item to set hierarchy"),
57677
+ weight: exports_external.coerce.number().optional().describe("Weight of the work item"),
57678
+ health_status: exports_external.enum(["onTrack", "needsAttention", "atRisk"]).optional().describe("Set health status"),
57679
+ start_date: exports_external.string().optional().describe("Start date in YYYY-MM-DD format"),
57680
+ due_date: exports_external.string().optional().describe("Due date in YYYY-MM-DD format"),
57681
+ milestone_id: exports_external.string().optional().describe("Milestone ID (GitLab global ID format, e.g. 'gid://gitlab/Milestone/123', or numeric ID)"),
57682
+ iteration_id: exports_external.string().optional().describe("Iteration ID (e.g. 'gid://gitlab/Iteration/123' or numeric ID). Use list_group_iterations to find available iterations."),
57683
+ confidential: exports_external.coerce.boolean().optional().describe("Set confidentiality")
57684
+ });
57685
+ var UpdateWorkItemSchema = WorkItemParamsSchema.extend({
57686
+ title: exports_external.string().optional().describe("New title"),
57687
+ description: exports_external.string().optional().describe("New description (Markdown supported)"),
57688
+ add_labels: exports_external.array(exports_external.string()).optional().describe("Label names to add"),
57689
+ remove_labels: exports_external.array(exports_external.string()).optional().describe("Label names to remove"),
57690
+ assignee_usernames: exports_external.array(exports_external.string()).optional().describe("Set assignees by username (replaces existing)"),
57691
+ state_event: exports_external.enum(["close", "reopen"]).optional().describe("Close or reopen the work item"),
57692
+ weight: exports_external.coerce.number().optional().describe("Set weight (issues, tasks, epics only)"),
57693
+ status: exports_external.string().optional().describe("Set status by ID. Use list_work_item_statuses to get available status IDs."),
57694
+ parent_iid: exports_external.coerce.number().optional().describe("Set parent work item by IID. Use with parent_project_id if parent is in a different project."),
57695
+ parent_project_id: exports_external.coerce.string().optional().describe("Project ID or path of the parent work item (defaults to same project as the work item)"),
57696
+ remove_parent: exports_external.coerce.boolean().optional().describe("Set to true to remove the parent from hierarchy"),
57697
+ children_to_add: exports_external.array(exports_external.object({
57698
+ project_id: exports_external.coerce.string().describe("Project ID or path of the child work item"),
57699
+ iid: exports_external.coerce.number().describe("IID of the child work item")
57700
+ })).optional().describe("Array of children to add to this work item's hierarchy"),
57701
+ children_to_remove: exports_external.array(exports_external.object({
57702
+ project_id: exports_external.coerce.string().describe("Project ID or path of the child work item"),
57703
+ iid: exports_external.coerce.number().describe("IID of the child work item")
57704
+ })).optional().describe("Array of children to remove from this work item's hierarchy"),
57705
+ health_status: exports_external.enum(["onTrack", "needsAttention", "atRisk"]).optional().describe("Set health status on issues and epics"),
57706
+ start_date: exports_external.string().optional().describe("Start date in YYYY-MM-DD format"),
57707
+ due_date: exports_external.string().optional().describe("Due date in YYYY-MM-DD format"),
57708
+ milestone_id: exports_external.string().optional().describe("Milestone ID (GitLab global ID format, e.g. 'gid://gitlab/Milestone/123', or numeric ID)"),
57709
+ iteration_id: exports_external.string().optional().describe("Iteration ID (e.g. 'gid://gitlab/Iteration/123' or numeric ID). Use list_group_iterations to find available iterations."),
57710
+ confidential: exports_external.coerce.boolean().optional().describe("Set confidentiality"),
57711
+ linked_items_to_add: exports_external.array(exports_external.object({
57712
+ project_id: exports_external.coerce.string().describe("Project ID or path of the work item to link"),
57713
+ iid: exports_external.coerce.number().describe("IID of the work item to link"),
57714
+ link_type: exports_external.enum(["RELATED", "BLOCKED_BY", "BLOCKS"]).optional().default("RELATED").describe("Link type: RELATED, BLOCKED_BY, or BLOCKS. Defaults to RELATED.")
57715
+ })).optional().describe("Work items to link"),
57716
+ linked_items_to_remove: exports_external.array(exports_external.object({
57717
+ project_id: exports_external.coerce.string().describe("Project ID or path of the linked work item to remove"),
57718
+ iid: exports_external.coerce.number().describe("IID of the linked work item to remove")
57719
+ })).optional().describe("Linked work items to remove"),
57720
+ custom_fields: exports_external.array(exports_external.object({
57721
+ custom_field_id: exports_external.string().describe("Custom field ID (e.g. 'gid://gitlab/IssuablesCustomField/123' or numeric ID)"),
57722
+ text_value: exports_external.string().optional().describe("Text value (for text fields)"),
57723
+ number_value: exports_external.coerce.number().optional().describe("Number value (for number fields)"),
57724
+ selected_option_ids: exports_external.array(exports_external.string()).optional().describe("Selected option IDs (for select fields)"),
57725
+ date_value: exports_external.string().optional().describe("Date value in YYYY-MM-DD format (for date fields)")
57726
+ })).optional().describe("Custom field values to set"),
57727
+ severity: exports_external.enum(["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional().describe("Incident only: set severity level"),
57728
+ escalation_status: exports_external.enum(["TRIGGERED", "ACKNOWLEDGED", "RESOLVED", "IGNORED"]).optional().describe("Incident only: set escalation status")
57729
+ });
57730
+ var ConvertWorkItemTypeSchema = exports_external.object({
57731
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57732
+ iid: exports_external.coerce.number().describe("The internal ID of the work item"),
57733
+ new_type: workItemTypeEnum.describe("The target work item type to convert to")
57734
+ });
57735
+ var ListWorkItemStatusesSchema = exports_external.object({
57736
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57737
+ work_item_type: workItemTypeEnum.optional().default("issue").describe("The work item type to list available statuses for. Defaults to 'issue'.")
57738
+ });
57739
+ var ListWorkItemNotesSchema = exports_external.object({
57740
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57741
+ iid: exports_external.coerce.number().describe("The internal ID of the work item"),
57742
+ page_size: exports_external.coerce.number().optional().default(20).describe("Number of discussions to return (default 20)"),
57743
+ after: exports_external.string().optional().describe("Cursor for pagination"),
57744
+ sort: exports_external.enum(["CREATED_ASC", "CREATED_DESC"]).optional().default("CREATED_ASC").describe("Sort order for discussions")
57745
+ });
57746
+ var CreateWorkItemNoteSchema = exports_external.object({
57747
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57748
+ iid: exports_external.coerce.number().describe("The internal ID of the work item"),
57749
+ body: exports_external.string().describe("Note body (Markdown supported)"),
57750
+ internal: exports_external.coerce.boolean().optional().default(false).describe("Create as internal/confidential note (only visible to project members)"),
57751
+ discussion_id: exports_external.string().optional().describe("Discussion ID to reply to (for threaded replies). If omitted, creates a new top-level note.")
57752
+ });
57753
+ var MoveWorkItemSchema = exports_external.object({
57754
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path of the source project"),
57755
+ iid: exports_external.coerce.number().describe("The internal ID of the work item to move"),
57756
+ target_project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path of the target project")
57757
+ });
57758
+ var ListCustomFieldDefinitionsSchema = exports_external.object({
57759
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57760
+ work_item_type: workItemTypeEnum.optional().default("issue").describe("The work item type to list custom field definitions for. Defaults to 'issue'.")
57761
+ });
57762
+ var GetTimelineEventsSchema = exports_external.object({
57763
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57764
+ incident_iid: exports_external.coerce.number().describe("The internal ID (IID) of the incident")
57765
+ });
57766
+ var CreateTimelineEventSchema = exports_external.object({
57767
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
57768
+ incident_iid: exports_external.coerce.number().describe("The internal ID (IID) of the incident"),
57769
+ note: exports_external.string().describe("Description of the timeline event (Markdown supported)"),
57770
+ occurred_at: exports_external.string().describe("When the event occurred in ISO 8601 format (e.g. '2026-03-15T09:00:00.000Z')"),
57771
+ tag_names: exports_external.array(exports_external.enum([
57772
+ "Start time",
57773
+ "End time",
57774
+ "Impact detected",
57775
+ "Response initiated",
57776
+ "Impact mitigated",
57777
+ "Cause identified"
57778
+ ])).optional().describe("Timeline event tags to attach. Available: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'.")
57779
+ });
57780
+ async function resolveProjectPath(projectId) {
57781
+ const project = await defaultClient.get(`/projects/${encodeProjectId(projectId)}`);
57782
+ return project.path_with_namespace;
57783
+ }
57784
+ async function resolveWorkItemGID(projectId, issueIid) {
57785
+ const projectPath = await resolveProjectPath(projectId);
57786
+ const data = await defaultClient.graphql(`query($path: ID!, $iid: String!) {
57787
+ namespace(fullPath: $path) {
57788
+ workItem(iid: $iid) { id }
57789
+ }
57790
+ }`, { path: projectPath, iid: String(issueIid) });
57791
+ if (!data.namespace?.workItem?.id) {
57792
+ throw new Error(`Work item #${issueIid} not found in project ${projectPath}`);
57793
+ }
57794
+ return { workItemGID: data.namespace.workItem.id, projectPath };
57795
+ }
57796
+ async function resolveNamesToIds(projectPath, labelNames, usernames) {
57797
+ if (!labelNames?.length && !usernames?.length) {
57798
+ return { labelIds: [], userIds: [] };
57799
+ }
57800
+ const data = await defaultClient.graphql(`query($path: ID!, $usernames: [String!]!) {
57801
+ project(fullPath: $path) { labels(includeAncestorGroups: true, first: 250) { nodes { id title } } }
57802
+ users(usernames: $usernames) { nodes { id username } }
57803
+ }`, { path: projectPath, usernames: usernames || [] });
57804
+ const labelIds = (labelNames || []).map((name) => {
57805
+ const label = data.project.labels.nodes.find((l) => l.title === name);
57806
+ if (!label)
57807
+ throw new Error(`Label '${name}' not found in project`);
57808
+ return label.id;
57809
+ });
57810
+ const userIds = (usernames || []).map((name) => {
57811
+ const user = data.users.nodes.find((u) => u.username === name);
57812
+ if (!user)
57813
+ throw new Error(`User '${name}' not found`);
57814
+ return user.id;
57815
+ });
57816
+ return { labelIds, userIds };
57817
+ }
57818
+ async function resolveWorkItemTypeGID(projectPath, typeName) {
57819
+ const targetName = WORK_ITEM_TYPE_NAMES[typeName];
57820
+ if (!targetName)
57821
+ throw new Error(`Unknown work item type: ${typeName}`);
57822
+ const data = await defaultClient.graphql(`query($path: ID!) {
57823
+ namespace(fullPath: $path) {
57824
+ workItemTypes { nodes { id name } }
57825
+ }
57826
+ }`, { path: projectPath });
57827
+ const typeNode = data.namespace?.workItemTypes?.nodes?.find((n) => n.name === targetName);
57828
+ if (!typeNode) {
57829
+ throw new Error(`Work item type '${targetName}' not found in project ${projectPath}`);
57830
+ }
57831
+ return typeNode.id;
57832
+ }
57833
+ async function updateIncidentSeverity(projectPath, incidentIid, severity) {
57834
+ const data = await defaultClient.graphql(`mutation($projectPath: ID!, $severity: IssuableSeverity!, $iid: String!) {
57835
+ issueSetSeverity(input: { iid: $iid, severity: $severity, projectPath: $projectPath }) {
57836
+ errors
57837
+ }
57838
+ }`, { projectPath, severity, iid: String(incidentIid) });
57839
+ if (data.issueSetSeverity.errors?.length > 0) {
57840
+ throw new Error(`Failed to set severity: ${data.issueSetSeverity.errors.join(", ")}`);
57841
+ }
57842
+ }
57843
+ async function updateIncidentEscalationStatus(projectPath, incidentIid, status) {
57844
+ const data = await defaultClient.graphql(`mutation($projectPath: ID!, $status: IssueEscalationStatus!, $iid: String!) {
57845
+ issueSetEscalationStatus(input: { projectPath: $projectPath, status: $status, iid: $iid }) {
57846
+ errors
57847
+ }
57848
+ }`, { projectPath, status, iid: String(incidentIid) });
57849
+ if (data.issueSetEscalationStatus.errors?.length > 0) {
57850
+ throw new Error(`Failed to set escalation status: ${data.issueSetEscalationStatus.errors.join(", ")}`);
57851
+ }
57852
+ }
57853
+ async function removeIssueParent(projectId, issueIid) {
57854
+ const { workItemGID } = await resolveWorkItemGID(projectId, issueIid);
57855
+ const data = await defaultClient.graphql(`mutation($id: WorkItemID!) {
57856
+ workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: null } }) {
57857
+ workItem { id }
57858
+ errors
57859
+ }
57860
+ }`, { id: workItemGID });
57861
+ if (data.workItemUpdate.errors?.length > 0) {
57862
+ throw new Error(`Failed to remove parent: ${data.workItemUpdate.errors.join(", ")}`);
57863
+ }
57864
+ }
57865
+ function toGID(id, type) {
57866
+ return id.startsWith("gid://") ? id : `gid://gitlab/${type}/${id}`;
57867
+ }
57868
+ async function getWorkItem(projectId, iid) {
57869
+ const projectPath = await resolveProjectPath(projectId);
57870
+ const data = await defaultClient.graphql(`query($path: ID!, $iid: String!) {
57871
+ namespace(fullPath: $path) {
57872
+ workItem(iid: $iid) {
57873
+ id iid title state description webUrl confidential
57874
+ author { username }
57875
+ createdAt closedAt
57876
+ workItemType { name }
57877
+ widgets {
57878
+ __typename
57879
+ ... on WorkItemWidgetHierarchy {
57880
+ hasChildren hasParent
57881
+ parent { id iid title webUrl workItemType { name } namespace { fullPath } }
57882
+ children { nodes { id iid title state webUrl workItemType { name } namespace { fullPath } } }
57883
+ }
57884
+ ... on WorkItemWidgetStatus { status { id name category color iconName position } }
57885
+ ... on WorkItemWidgetCustomFields {
57886
+ customFieldValues {
57887
+ __typename
57888
+ customField { id name fieldType }
57889
+ ... on WorkItemNumberFieldValue { value }
57890
+ ... on WorkItemTextFieldValue { value }
57891
+ ... on WorkItemSelectFieldValue { selectedOptions { id value } }
57892
+ }
57893
+ }
57894
+ ... on WorkItemWidgetLabels { labels { nodes { id title color } } }
57895
+ ... on WorkItemWidgetAssignees { assignees { nodes { id username name } } }
57896
+ ... on WorkItemWidgetWeight { weight rolledUpWeight rolledUpCompletedWeight }
57897
+ ... on WorkItemWidgetHealthStatus { healthStatus }
57898
+ ... on WorkItemWidgetStartAndDueDate { startDate dueDate }
57899
+ ... on WorkItemWidgetMilestone { milestone { id title } }
57900
+ ... on WorkItemWidgetLinkedItems {
57901
+ blocked blockedByCount blockingCount
57902
+ linkedItems { nodes { linkType workItem { id iid title state webUrl workItemType { name } namespace { fullPath } } } }
57903
+ }
57904
+ ... on WorkItemWidgetTimeTracking { timeEstimate totalTimeSpent }
57905
+ ... on WorkItemWidgetDevelopment {
57906
+ willAutoCloseByMergeRequest
57907
+ relatedBranches { nodes { name } }
57908
+ relatedMergeRequests { nodes { iid title webUrl state sourceBranch } }
57909
+ closingMergeRequests { nodes { mergeRequest { iid title webUrl state sourceBranch } } }
57910
+ featureFlags { nodes { name active } }
57911
+ }
57912
+ ... on WorkItemWidgetIteration {
57913
+ iteration { id title startDate dueDate webUrl iterationCadence { id title } }
57914
+ }
57915
+ ... on WorkItemWidgetProgress { progress }
57916
+ ... on WorkItemWidgetColor { color textColor }
57917
+ }
57918
+ }
57919
+ }
57920
+ }`, { path: projectPath, iid: String(iid) });
57921
+ if (!data.namespace?.workItem) {
57922
+ throw new Error(`Work item #${iid} not found in project ${projectPath}`);
57923
+ }
57924
+ return flattenWorkItem(data.namespace.workItem);
57925
+ }
57926
+ function findWidget(widgets, typeName) {
57927
+ return widgets.find((w) => w.__typename === typeName);
57928
+ }
57929
+ function extractCoreWidgets(widgets, result) {
57930
+ const statusW = findWidget(widgets, "WorkItemWidgetStatus");
57931
+ if (statusW?.status)
57932
+ result.status = {
57933
+ name: statusW.status.name,
57934
+ id: statusW.status.id,
57935
+ category: statusW.status.category
57936
+ };
57937
+ const labelsW = findWidget(widgets, "WorkItemWidgetLabels");
57938
+ const labels = (labelsW?.labels?.nodes || []).map((l) => l.title);
57939
+ if (labels.length > 0)
57940
+ result.labels = labels;
57941
+ const assigneesW = findWidget(widgets, "WorkItemWidgetAssignees");
57942
+ const assignees = (assigneesW?.assignees?.nodes || []).map((a) => a.username);
57943
+ if (assignees.length > 0)
57944
+ result.assignees = assignees;
57945
+ const weightW = findWidget(widgets, "WorkItemWidgetWeight");
57946
+ if (weightW?.weight != null) {
57947
+ result.weight = weightW.weight;
57948
+ if (weightW.rolledUpWeight != null)
57949
+ result.rolledUpWeight = weightW.rolledUpWeight;
57950
+ if (weightW.rolledUpCompletedWeight != null)
57951
+ result.rolledUpCompletedWeight = weightW.rolledUpCompletedWeight;
57952
+ }
57953
+ const healthW = findWidget(widgets, "WorkItemWidgetHealthStatus");
57954
+ if (healthW?.healthStatus)
57955
+ result.healthStatus = healthW.healthStatus;
57956
+ }
57957
+ function extractMetadataWidgets(widgets, result) {
57958
+ const datesW = findWidget(widgets, "WorkItemWidgetStartAndDueDate");
57959
+ if (datesW?.startDate)
57960
+ result.startDate = datesW.startDate;
57961
+ if (datesW?.dueDate)
57962
+ result.dueDate = datesW.dueDate;
57963
+ const milestoneW = findWidget(widgets, "WorkItemWidgetMilestone");
57964
+ if (milestoneW?.milestone)
57965
+ result.milestone = { id: milestoneW.milestone.id, title: milestoneW.milestone.title };
57966
+ const iterW = findWidget(widgets, "WorkItemWidgetIteration");
57967
+ if (iterW?.iteration) {
57968
+ result.iteration = {
57969
+ id: iterW.iteration.id,
57970
+ title: iterW.iteration.title,
57971
+ startDate: iterW.iteration.startDate,
57972
+ dueDate: iterW.iteration.dueDate
57973
+ };
57974
+ }
57975
+ const progressW = findWidget(widgets, "WorkItemWidgetProgress");
57976
+ if (progressW?.progress != null)
57977
+ result.progress = progressW.progress;
57978
+ const colorW = findWidget(widgets, "WorkItemWidgetColor");
57979
+ if (colorW?.color)
57980
+ result.color = colorW.color;
57981
+ const timeW = findWidget(widgets, "WorkItemWidgetTimeTracking");
57982
+ if (timeW?.timeEstimate > 0)
57983
+ result.timeEstimate = timeW.timeEstimate;
57984
+ if (timeW?.totalTimeSpent > 0)
57985
+ result.totalTimeSpent = timeW.totalTimeSpent;
57986
+ }
57987
+ function extractHierarchyWidget(widgets, result) {
57988
+ const hierW = findWidget(widgets, "WorkItemWidgetHierarchy");
57989
+ if (hierW?.parent)
57990
+ result.parent = {
57991
+ iid: hierW.parent.iid,
57992
+ title: hierW.parent.title,
57993
+ type: hierW.parent.workItemType?.name,
57994
+ project: hierW.parent.namespace?.fullPath,
57995
+ webUrl: hierW.parent.webUrl
57996
+ };
57997
+ const children = hierW?.children?.nodes || [];
57998
+ if (children.length > 0)
57999
+ result.children = children.map((c) => ({
58000
+ iid: c.iid,
58001
+ title: c.title,
58002
+ state: c.state,
58003
+ type: c.workItemType?.name,
58004
+ project: c.namespace?.fullPath,
58005
+ webUrl: c.webUrl
58006
+ }));
58007
+ }
58008
+ function extractLinkedItemsWidget(widgets, result) {
58009
+ const linkedW = findWidget(widgets, "WorkItemWidgetLinkedItems");
58010
+ if (linkedW?.blocked)
58011
+ result.blocked = true;
58012
+ if (linkedW?.blockedByCount > 0)
58013
+ result.blockedByCount = linkedW.blockedByCount;
58014
+ if (linkedW?.blockingCount > 0)
58015
+ result.blockingCount = linkedW.blockingCount;
58016
+ const linkedNodes = linkedW?.linkedItems?.nodes || [];
58017
+ if (linkedNodes.length > 0) {
58018
+ result.linkedItems = linkedNodes.map((n) => ({
58019
+ linkType: n.linkType,
58020
+ iid: n.workItem?.iid,
58021
+ title: n.workItem?.title,
58022
+ state: n.workItem?.state,
58023
+ type: n.workItem?.workItemType?.name,
58024
+ project: n.workItem?.namespace?.fullPath,
58025
+ webUrl: n.workItem?.webUrl
58026
+ }));
58027
+ }
58028
+ }
58029
+ function extractDevelopmentWidget(widgets, result) {
58030
+ const devW = findWidget(widgets, "WorkItemWidgetDevelopment");
58031
+ const relatedMRs = devW?.relatedMergeRequests?.nodes || [];
58032
+ const closingMRs = (devW?.closingMergeRequests?.nodes || []).map((n) => n.mergeRequest);
58033
+ const branches = devW?.relatedBranches?.nodes || [];
58034
+ const flags = devW?.featureFlags?.nodes || [];
58035
+ if (relatedMRs.length > 0 || closingMRs.length > 0 || branches.length > 0 || flags.length > 0) {
58036
+ const dev = {};
58037
+ if (relatedMRs.length > 0)
58038
+ dev.relatedMergeRequests = relatedMRs;
58039
+ if (closingMRs.length > 0)
58040
+ dev.closingMergeRequests = closingMRs;
58041
+ if (branches.length > 0)
58042
+ dev.relatedBranches = branches.map((b) => b.name);
58043
+ if (flags.length > 0)
58044
+ dev.featureFlags = flags;
58045
+ result.development = dev;
58046
+ }
58047
+ }
58048
+ function extractCustomFieldsWidget(widgets, result) {
58049
+ const cfW = findWidget(widgets, "WorkItemWidgetCustomFields");
58050
+ const cfValues = (cfW?.customFieldValues || []).filter((cfv) => cfv.value != null || cfv.selectedOptions != null);
58051
+ if (cfValues.length > 0) {
58052
+ result.customFields = cfValues.map((cfv) => ({
58053
+ name: cfv.customField?.name,
58054
+ type: cfv.customField?.fieldType,
58055
+ value: cfv.value ?? cfv.selectedOptions ?? null
58056
+ }));
58057
+ }
58058
+ }
58059
+ function extractRelationWidgets(widgets, result) {
58060
+ extractHierarchyWidget(widgets, result);
58061
+ extractLinkedItemsWidget(widgets, result);
58062
+ extractDevelopmentWidget(widgets, result);
58063
+ extractCustomFieldsWidget(widgets, result);
58064
+ }
58065
+ function flattenWorkItem(wi) {
58066
+ const widgets = wi.widgets || [];
58067
+ const result = {
58068
+ id: wi.id,
58069
+ iid: wi.iid,
58070
+ title: wi.title,
58071
+ state: wi.state,
58072
+ type: wi.workItemType?.name,
58073
+ webUrl: wi.webUrl
58074
+ };
58075
+ if (wi.description)
58076
+ result.description = wi.description;
58077
+ if (wi.confidential)
58078
+ result.confidential = true;
58079
+ if (wi.author?.username)
58080
+ result.author = wi.author.username;
58081
+ if (wi.createdAt)
58082
+ result.createdAt = wi.createdAt;
58083
+ if (wi.closedAt)
58084
+ result.closedAt = wi.closedAt;
58085
+ extractCoreWidgets(widgets, result);
58086
+ extractMetadataWidgets(widgets, result);
58087
+ extractRelationWidgets(widgets, result);
58088
+ return result;
58089
+ }
58090
+ function flattenWorkItemSummary(wi) {
58091
+ const widgets = wi.widgets || [];
58092
+ const item = {
58093
+ iid: wi.iid,
58094
+ title: wi.title,
58095
+ state: wi.state,
58096
+ type: wi.workItemType?.name,
58097
+ webUrl: wi.webUrl
58098
+ };
58099
+ const statusW = findWidget(widgets, "WorkItemWidgetStatus");
58100
+ if (statusW?.status)
58101
+ item.status = statusW.status.name;
58102
+ const labelsW = findWidget(widgets, "WorkItemWidgetLabels");
58103
+ const labels = (labelsW?.labels?.nodes || []).map((l) => l.title);
58104
+ if (labels.length > 0)
58105
+ item.labels = labels;
58106
+ const assigneesW = findWidget(widgets, "WorkItemWidgetAssignees");
58107
+ const assignees = (assigneesW?.assignees?.nodes || []).map((a) => a.username);
58108
+ if (assignees.length > 0)
58109
+ item.assignees = assignees;
58110
+ const weightW = findWidget(widgets, "WorkItemWidgetWeight");
58111
+ if (weightW?.weight != null)
58112
+ item.weight = weightW.weight;
58113
+ const healthW = findWidget(widgets, "WorkItemWidgetHealthStatus");
58114
+ if (healthW?.healthStatus)
58115
+ item.healthStatus = healthW.healthStatus;
58116
+ const datesW = findWidget(widgets, "WorkItemWidgetStartAndDueDate");
58117
+ if (datesW?.startDate)
58118
+ item.startDate = datesW.startDate;
58119
+ if (datesW?.dueDate)
58120
+ item.dueDate = datesW.dueDate;
58121
+ const milestoneW = findWidget(widgets, "WorkItemWidgetMilestone");
58122
+ if (milestoneW?.milestone)
58123
+ item.milestone = milestoneW.milestone.title;
58124
+ return item;
58125
+ }
58126
+ async function listWorkItems(projectId, options) {
58127
+ const projectPath = await resolveProjectPath(projectId);
58128
+ const typeMap = {
58129
+ issue: "ISSUE",
58130
+ task: "TASK",
58131
+ incident: "INCIDENT",
58132
+ test_case: "TEST_CASE",
58133
+ epic: "EPIC",
58134
+ key_result: "KEY_RESULT",
58135
+ objective: "OBJECTIVE",
58136
+ requirement: "REQUIREMENT",
58137
+ ticket: "TICKET"
58138
+ };
58139
+ const variables = {
58140
+ path: projectPath,
58141
+ first: options.first || 20
58142
+ };
58143
+ if (options.types?.length)
58144
+ variables.types = options.types.map((t) => typeMap[t] || t.replace(/ /g, "_").toUpperCase());
58145
+ if (options.state)
58146
+ variables.state = options.state === "opened" ? "opened" : "closed";
58147
+ if (options.search)
58148
+ variables.search = options.search;
58149
+ if (options.assignee_usernames?.length)
58150
+ variables.assigneeUsernames = options.assignee_usernames;
58151
+ if (options.label_names?.length)
58152
+ variables.labelName = options.label_names;
58153
+ if (options.after)
58154
+ variables.after = options.after;
58155
+ const data = await defaultClient.graphql(`query($path: ID!, $types: [IssueType!], $state: IssuableState, $search: String, $assigneeUsernames: [String!], $labelName: [String!], $first: Int, $after: String) {
58156
+ project(fullPath: $path) {
58157
+ workItems(types: $types, state: $state, search: $search, assigneeUsernames: $assigneeUsernames, labelName: $labelName, first: $first, after: $after) {
58158
+ nodes {
58159
+ id iid title state webUrl workItemType { name }
58160
+ widgets {
58161
+ __typename
58162
+ ... on WorkItemWidgetStatus { status { id name category color } }
58163
+ ... on WorkItemWidgetLabels { labels { nodes { title } } }
58164
+ ... on WorkItemWidgetAssignees { assignees { nodes { username } } }
58165
+ ... on WorkItemWidgetWeight { weight }
58166
+ ... on WorkItemWidgetHealthStatus { healthStatus }
58167
+ ... on WorkItemWidgetStartAndDueDate { startDate dueDate }
58168
+ ... on WorkItemWidgetMilestone { milestone { id title } }
58169
+ }
58170
+ }
58171
+ pageInfo { hasNextPage endCursor }
58172
+ }
58173
+ }
58174
+ }`, variables);
58175
+ const workItems = data.project?.workItems?.nodes || [];
58176
+ const pageInfo = data.project?.workItems?.pageInfo || {};
58177
+ const items = workItems.map(flattenWorkItemSummary);
58178
+ return { items, pageInfo };
58179
+ }
58180
+ function addCommonWidgetFields(builder, options) {
58181
+ if (options.weight !== undefined) {
58182
+ builder.varDefs.push("$weight: Int");
58183
+ builder.inputParts.push("weightWidget: { weight: $weight }");
58184
+ builder.variables.weight = options.weight;
58185
+ }
58186
+ if (options.health_status !== undefined) {
58187
+ builder.varDefs.push("$healthStatus: HealthStatus");
58188
+ builder.inputParts.push("healthStatusWidget: { healthStatus: $healthStatus }");
58189
+ builder.variables.healthStatus = options.health_status;
58190
+ }
58191
+ if (options.start_date !== undefined || options.due_date !== undefined) {
58192
+ const dateParts = [];
58193
+ if (options.start_date !== undefined) {
58194
+ builder.varDefs.push("$startDate: Date");
58195
+ dateParts.push("startDate: $startDate");
58196
+ builder.variables.startDate = options.start_date;
58197
+ }
58198
+ if (options.due_date !== undefined) {
58199
+ builder.varDefs.push("$dueDate: Date");
58200
+ dateParts.push("dueDate: $dueDate");
58201
+ builder.variables.dueDate = options.due_date;
58202
+ }
58203
+ builder.inputParts.push(`startAndDueDateWidget: { ${dateParts.join(", ")} }`);
58204
+ }
58205
+ if (options.milestone_id !== undefined) {
58206
+ builder.varDefs.push("$milestoneId: MilestoneID");
58207
+ builder.inputParts.push("milestoneWidget: { milestoneId: $milestoneId }");
58208
+ builder.variables.milestoneId = toGID(options.milestone_id, "Milestone");
58209
+ }
58210
+ if (options.iteration_id !== undefined) {
58211
+ builder.varDefs.push("$iterationId: IterationID");
58212
+ builder.inputParts.push("iterationWidget: { iterationId: $iterationId }");
58213
+ builder.variables.iterationId = toGID(options.iteration_id, "Iteration");
58214
+ }
58215
+ if (options.confidential !== undefined) {
58216
+ builder.varDefs.push("$confidential: Boolean");
58217
+ builder.inputParts.push("confidential: $confidential");
58218
+ builder.variables.confidential = options.confidential;
58219
+ }
58220
+ }
58221
+ async function createWorkItem(projectId, options) {
58222
+ const projectPath = await resolveProjectPath(projectId);
58223
+ const typeName = options.type || "issue";
58224
+ const typeGID = await resolveWorkItemTypeGID(projectPath, typeName);
58225
+ const builder = {
58226
+ varDefs: ["$projectPath: ID!", "$title: String!", "$typeId: WorkItemsTypeID!"],
58227
+ inputParts: ["namespacePath: $projectPath", "title: $title", "workItemTypeId: $typeId"],
58228
+ variables: { projectPath, title: options.title, typeId: typeGID }
58229
+ };
58230
+ if (options.description !== undefined) {
58231
+ builder.varDefs.push("$description: String!");
58232
+ builder.inputParts.push("descriptionWidget: { description: $description }");
58233
+ builder.variables.description = options.description;
58234
+ }
58235
+ const { labelIds, userIds } = await resolveNamesToIds(projectPath, options.labels, options.assignee_usernames);
58236
+ if (labelIds.length > 0) {
58237
+ builder.varDefs.push("$labelIds: [LabelID!]!");
58238
+ builder.inputParts.push("labelsWidget: { labelIds: $labelIds }");
58239
+ builder.variables.labelIds = labelIds;
58240
+ }
58241
+ if (options.parent_iid !== undefined) {
58242
+ const { workItemGID: parentGID } = await resolveWorkItemGID(projectId, options.parent_iid);
58243
+ builder.varDefs.push("$parentId: WorkItemID");
58244
+ builder.inputParts.push("hierarchyWidget: { parentId: $parentId }");
58245
+ builder.variables.parentId = parentGID;
58246
+ }
58247
+ if (userIds.length > 0) {
58248
+ builder.varDefs.push("$assigneeIds: [UserID!]!");
58249
+ builder.inputParts.push("assigneesWidget: { assigneeIds: $assigneeIds }");
58250
+ builder.variables.assigneeIds = userIds;
58251
+ }
58252
+ addCommonWidgetFields(builder, options);
58253
+ const mutation = `mutation(${builder.varDefs.join(", ")}) {
58254
+ workItemCreate(input: { ${builder.inputParts.join(", ")} }) {
58255
+ workItem { id iid title webUrl workItemType { name } }
58256
+ errors
58257
+ }
58258
+ }`;
58259
+ const data = await defaultClient.graphql(mutation, builder.variables);
58260
+ if (data.workItemCreate.errors?.length > 0) {
58261
+ throw new Error(`Failed to create work item: ${data.workItemCreate.errors.join(", ")}`);
58262
+ }
58263
+ const wi = data.workItemCreate.workItem;
58264
+ return {
58265
+ id: wi.id,
58266
+ iid: wi.iid,
58267
+ title: wi.title,
58268
+ type: wi.workItemType?.name,
58269
+ webUrl: wi.webUrl
58270
+ };
58271
+ }
58272
+ async function buildUpdateMutationFields(projectId, projectPath, workItemGID, options) {
58273
+ const builder = {
58274
+ varDefs: ["$id: WorkItemID!"],
58275
+ inputParts: ["id: $id"],
58276
+ variables: { id: workItemGID }
58277
+ };
58278
+ if (options.title !== undefined) {
58279
+ builder.varDefs.push("$title: String");
58280
+ builder.inputParts.push("title: $title");
58281
+ builder.variables.title = options.title;
58282
+ }
58283
+ if (options.description !== undefined) {
58284
+ builder.varDefs.push("$description: String!");
58285
+ builder.inputParts.push("descriptionWidget: { description: $description }");
58286
+ builder.variables.description = options.description;
58287
+ }
58288
+ addUpdateLabelsAndAssignees(builder, projectPath, options);
58289
+ if (options.state_event !== undefined) {
58290
+ builder.varDefs.push("$stateEvent: WorkItemStateEvent");
58291
+ builder.inputParts.push("stateEvent: $stateEvent");
58292
+ builder.variables.stateEvent = options.state_event === "close" ? "CLOSE" : "REOPEN";
58293
+ }
58294
+ if (options.status !== undefined) {
58295
+ builder.varDefs.push("$status: WorkItemsStatusesStatusID");
58296
+ builder.inputParts.push("statusWidget: { status: $status }");
58297
+ builder.variables.status = options.status;
58298
+ }
58299
+ addCommonWidgetFields(builder, {
58300
+ weight: options.weight,
58301
+ health_status: options.health_status,
58302
+ start_date: options.start_date,
58303
+ due_date: options.due_date,
58304
+ milestone_id: options.milestone_id,
58305
+ iteration_id: options.iteration_id,
58306
+ confidential: options.confidential
58307
+ });
58308
+ addUpdateCustomFields(builder, options);
58309
+ await addUpdateHierarchy(builder, projectId, options);
58310
+ return builder;
58311
+ }
58312
+ async function addUpdateLabelsAndAssignees(builder, projectPath, options) {
58313
+ const allLabelNames = [
58314
+ ...options.add_labels || [],
58315
+ ...options.remove_labels || []
58316
+ ];
58317
+ const needsResolve = allLabelNames.length > 0 || options.assignee_usernames?.length;
58318
+ const { labelIds: resolvedLabelIds, userIds } = needsResolve ? await resolveNamesToIds(projectPath, allLabelNames.length > 0 ? allLabelNames : undefined, options.assignee_usernames) : { labelIds: [], userIds: [] };
58319
+ if (options.add_labels || options.remove_labels) {
58320
+ const labelParts = [];
58321
+ let offset = 0;
58322
+ if (options.add_labels?.length) {
58323
+ const addIds = resolvedLabelIds.slice(0, options.add_labels.length);
58324
+ offset = options.add_labels.length;
58325
+ builder.varDefs.push("$addLabelIds: [LabelID!]");
58326
+ labelParts.push("addLabelIds: $addLabelIds");
58327
+ builder.variables.addLabelIds = addIds;
58328
+ }
58329
+ if (options.remove_labels?.length) {
58330
+ const removeIds = resolvedLabelIds.slice(offset);
58331
+ builder.varDefs.push("$removeLabelIds: [LabelID!]");
58332
+ labelParts.push("removeLabelIds: $removeLabelIds");
58333
+ builder.variables.removeLabelIds = removeIds;
58334
+ }
58335
+ if (labelParts.length > 0) {
58336
+ builder.inputParts.push(`labelsWidget: { ${labelParts.join(", ")} }`);
58337
+ }
58338
+ }
58339
+ if (userIds.length > 0) {
58340
+ builder.varDefs.push("$assigneeIds: [UserID!]!");
58341
+ builder.inputParts.push("assigneesWidget: { assigneeIds: $assigneeIds }");
58342
+ builder.variables.assigneeIds = userIds;
58343
+ }
58344
+ }
58345
+ function addUpdateCustomFields(builder, options) {
58346
+ if (!options.custom_fields || !options.custom_fields.length)
58347
+ return;
58348
+ const cfInput = options.custom_fields.map((cf) => {
58349
+ const val = {
58350
+ customFieldId: toGID(cf.custom_field_id, "IssuablesCustomField")
58351
+ };
58352
+ if (cf.text_value !== undefined)
58353
+ val.textValue = cf.text_value;
58354
+ if (cf.number_value !== undefined)
58355
+ val.numberValue = cf.number_value;
58356
+ if (cf.selected_option_ids !== undefined)
58357
+ val.selectedOptionIds = cf.selected_option_ids;
58358
+ if (cf.date_value !== undefined)
58359
+ val.dateValue = cf.date_value;
58360
+ return val;
58361
+ });
58362
+ builder.varDefs.push("$customFieldsWidget: [WorkItemWidgetCustomFieldValueInputType!]");
58363
+ builder.inputParts.push("customFieldsWidget: $customFieldsWidget");
58364
+ builder.variables.customFieldsWidget = cfInput;
58365
+ }
58366
+ async function addUpdateHierarchy(builder, projectId, options) {
58367
+ if (options.remove_parent) {
58368
+ builder.inputParts.push("hierarchyWidget: { parentId: null }");
58369
+ } else if (options.parent_iid !== undefined) {
58370
+ const parentProjectId = options.parent_project_id || projectId;
58371
+ const { workItemGID: parentGID } = await resolveWorkItemGID(parentProjectId, options.parent_iid);
58372
+ builder.varDefs.push("$parentId: WorkItemID");
58373
+ builder.inputParts.push("hierarchyWidget: { parentId: $parentId }");
58374
+ builder.variables.parentId = parentGID;
58375
+ }
58376
+ }
58377
+ async function handleChildrenToAdd(workItemGID, options) {
58378
+ const childrenToAdd = options.children_to_add;
58379
+ if (!childrenToAdd?.length)
58380
+ return;
58381
+ const childGIDs = [];
58382
+ for (const child of childrenToAdd) {
58383
+ const { workItemGID: childGID } = await resolveWorkItemGID(child.project_id, child.iid);
58384
+ childGIDs.push(childGID);
58385
+ }
58386
+ const addData = await defaultClient.graphql(`mutation($id: WorkItemID!, $childrenIds: [WorkItemID!]!) {
58387
+ workItemUpdate(input: { id: $id, hierarchyWidget: { childrenIds: $childrenIds } }) { errors }
58388
+ }`, { id: workItemGID, childrenIds: childGIDs });
58389
+ if (addData.workItemUpdate.errors?.length > 0) {
58390
+ throw new Error(`Failed to add children: ${addData.workItemUpdate.errors.join(", ")}`);
58391
+ }
58392
+ }
58393
+ async function handleChildrenToRemove(options) {
58394
+ const childrenToRemove = options.children_to_remove;
58395
+ if (!childrenToRemove?.length)
58396
+ return;
58397
+ for (const child of childrenToRemove) {
58398
+ await removeIssueParent(child.project_id, child.iid);
58399
+ }
58400
+ }
58401
+ async function handleLinkedItemsToAdd(workItemGID, options) {
58402
+ const linkedToAdd = options.linked_items_to_add;
58403
+ if (!linkedToAdd?.length)
58404
+ return;
58405
+ const groupedByType = {};
58406
+ for (const item of linkedToAdd) {
58407
+ const linkType = item.link_type || "RELATED";
58408
+ if (!groupedByType[linkType])
58409
+ groupedByType[linkType] = [];
58410
+ const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id, item.iid);
58411
+ groupedByType[linkType].push(targetGID);
58412
+ }
58413
+ for (const [linkType, targetGIDs] of Object.entries(groupedByType)) {
58414
+ const addLinkedData = await defaultClient.graphql(`mutation($id: WorkItemID!, $workItemsIds: [WorkItemID!]!, $linkType: WorkItemRelatedLinkType!) {
58415
+ workItemAddLinkedItems(input: { id: $id, workItemsIds: $workItemsIds, linkType: $linkType }) { errors }
58416
+ }`, { id: workItemGID, workItemsIds: targetGIDs, linkType });
58417
+ if (addLinkedData.workItemAddLinkedItems.errors?.length > 0) {
58418
+ throw new Error(`Failed to add linked items: ${addLinkedData.workItemAddLinkedItems.errors.join(", ")}`);
58419
+ }
58420
+ }
58421
+ }
58422
+ async function handleLinkedItemsToRemove(workItemGID, options) {
58423
+ const linkedToRemove = options.linked_items_to_remove;
58424
+ if (!linkedToRemove?.length)
58425
+ return;
58426
+ const targetGIDs = [];
58427
+ for (const item of linkedToRemove) {
58428
+ const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id, item.iid);
58429
+ targetGIDs.push(targetGID);
58430
+ }
58431
+ const removeLinkedData = await defaultClient.graphql(`mutation($id: WorkItemID!, $workItemsIds: [WorkItemID!]!) {
58432
+ workItemRemoveLinkedItems(input: { id: $id, workItemsIds: $workItemsIds }) { errors }
58433
+ }`, { id: workItemGID, workItemsIds: targetGIDs });
58434
+ if (removeLinkedData.workItemRemoveLinkedItems.errors?.length > 0) {
58435
+ throw new Error(`Failed to remove linked items: ${removeLinkedData.workItemRemoveLinkedItems.errors.join(", ")}`);
58436
+ }
58437
+ }
58438
+ async function handleUpdateSideEffects(workItemGID, projectPath, iid, options) {
58439
+ await handleChildrenToAdd(workItemGID, options);
58440
+ await handleChildrenToRemove(options);
58441
+ await handleLinkedItemsToAdd(workItemGID, options);
58442
+ await handleLinkedItemsToRemove(workItemGID, options);
58443
+ if (options.severity !== undefined) {
58444
+ await updateIncidentSeverity(projectPath, iid, options.severity);
58445
+ }
58446
+ if (options.escalation_status !== undefined) {
58447
+ await updateIncidentEscalationStatus(projectPath, iid, options.escalation_status);
58448
+ }
58449
+ }
58450
+ function flattenUpdateWidgets(wi) {
58451
+ const widgets = wi?.widgets || [];
58452
+ const statusW = findWidget(widgets, "WorkItemWidgetStatus");
58453
+ const labelsW = findWidget(widgets, "WorkItemWidgetLabels");
58454
+ const assigneesW = findWidget(widgets, "WorkItemWidgetAssignees");
58455
+ const weightW = findWidget(widgets, "WorkItemWidgetWeight");
58456
+ const hierarchyW = findWidget(widgets, "WorkItemWidgetHierarchy");
58457
+ const healthStatusW = findWidget(widgets, "WorkItemWidgetHealthStatus");
58458
+ const datesW = findWidget(widgets, "WorkItemWidgetStartAndDueDate");
58459
+ const milestoneW = findWidget(widgets, "WorkItemWidgetMilestone");
58460
+ return {
58461
+ id: wi.id,
58462
+ iid: wi.iid,
58463
+ title: wi.title,
58464
+ state: wi.state,
58465
+ type: wi.workItemType?.name,
58466
+ webUrl: wi.webUrl,
58467
+ status: statusW?.status || null,
58468
+ labels: (labelsW?.labels?.nodes || []).map((l) => l.title),
58469
+ assignees: (assigneesW?.assignees?.nodes || []).map((a) => a.username),
58470
+ weight: weightW?.weight ?? null,
58471
+ parent: hierarchyW?.parent || null,
58472
+ healthStatus: healthStatusW?.healthStatus || null,
58473
+ startDate: datesW?.startDate || null,
58474
+ dueDate: datesW?.dueDate || null,
58475
+ milestone: milestoneW?.milestone || null
58476
+ };
58477
+ }
58478
+ function buildSideEffectCounts(options) {
58479
+ const counts = {
58480
+ children_added: options.children_to_add?.length || 0,
58481
+ children_removed: options.children_to_remove?.length || 0,
58482
+ linked_items_added: options.linked_items_to_add?.length || 0,
58483
+ linked_items_removed: options.linked_items_to_remove?.length || 0
58484
+ };
58485
+ if (options.severity !== undefined)
58486
+ counts.severity = options.severity;
58487
+ if (options.escalation_status !== undefined)
58488
+ counts.escalation_status = options.escalation_status;
58489
+ return counts;
58490
+ }
58491
+ async function updateWorkItem(projectId, iid, options) {
58492
+ const { workItemGID, projectPath } = await resolveWorkItemGID(projectId, iid);
58493
+ const builder = await buildUpdateMutationFields(projectId, projectPath, workItemGID, options);
58494
+ const mutation = `mutation(${builder.varDefs.join(", ")}) {
58495
+ workItemUpdate(input: { ${builder.inputParts.join(", ")} }) {
58496
+ workItem {
58497
+ id iid title state webUrl workItemType { name }
58498
+ widgets {
58499
+ __typename
58500
+ ... on WorkItemWidgetStatus { status { id name category color } }
58501
+ ... on WorkItemWidgetLabels { labels { nodes { title } } }
58502
+ ... on WorkItemWidgetAssignees { assignees { nodes { username } } }
58503
+ ... on WorkItemWidgetWeight { weight }
58504
+ ... on WorkItemWidgetHierarchy { parent { id title workItemType { name } } }
58505
+ ... on WorkItemWidgetHealthStatus { healthStatus }
58506
+ ... on WorkItemWidgetStartAndDueDate { startDate dueDate }
58507
+ ... on WorkItemWidgetMilestone { milestone { id title } }
58508
+ }
58509
+ }
58510
+ errors
58511
+ }
58512
+ }`;
58513
+ const data = await defaultClient.graphql(mutation, builder.variables);
58514
+ if (data.workItemUpdate.errors?.length > 0) {
58515
+ throw new Error(`Failed to update work item: ${data.workItemUpdate.errors.join(", ")}`);
58516
+ }
58517
+ await handleUpdateSideEffects(workItemGID, projectPath, iid, options);
58518
+ return {
58519
+ ...flattenUpdateWidgets(data.workItemUpdate.workItem),
58520
+ ...buildSideEffectCounts(options)
58521
+ };
58522
+ }
58523
+ async function convertWorkItemType(projectId, iid, newType) {
58524
+ const { workItemGID, projectPath } = await resolveWorkItemGID(projectId, iid);
58525
+ const workItemTypeGID = await resolveWorkItemTypeGID(projectPath, newType);
58526
+ const data = await defaultClient.graphql(`mutation($id: WorkItemID!, $typeId: WorkItemsTypeID!) {
58527
+ workItemConvert(input: { id: $id, workItemTypeId: $typeId }) {
58528
+ workItem { id workItemType { name } }
58529
+ errors
58530
+ }
58531
+ }`, { id: workItemGID, typeId: workItemTypeGID });
58532
+ if (data.workItemConvert.errors?.length > 0) {
58533
+ throw new Error(`Conversion failed: ${data.workItemConvert.errors.join(", ")}`);
58534
+ }
58535
+ const converted = data.workItemConvert.workItem;
58536
+ if (!converted) {
58537
+ throw new Error("Conversion succeeded but returned no work item");
58538
+ }
58539
+ return {
58540
+ id: converted.id,
58541
+ type: converted.workItemType.name
58542
+ };
58543
+ }
58544
+ async function listWorkItemStatuses(projectId, workItemType = "issue") {
58545
+ const projectPath = await resolveProjectPath(projectId);
58546
+ const typeName = WORK_ITEM_TYPE_NAMES[workItemType] || "Issue";
58547
+ const data = await defaultClient.graphql(`query($path: ID!, $typeName: IssueType) {
58548
+ namespace(fullPath: $path) {
58549
+ workItemTypes(name: $typeName) {
58550
+ nodes {
58551
+ id name
58552
+ supportedConversionTypes { id name }
58553
+ widgetDefinitions {
58554
+ __typename
58555
+ ... on WorkItemWidgetDefinitionStatus {
58556
+ allowedStatuses { id name iconName color position }
58557
+ }
58558
+ ... on WorkItemWidgetDefinitionHierarchy {
58559
+ allowedChildTypes { nodes { id name } }
58560
+ allowedParentTypes { nodes { id name } }
58561
+ }
58562
+ }
58563
+ }
58564
+ }
58565
+ }
58566
+ }`, { path: projectPath, typeName: typeName.replace(/ /g, "_").toUpperCase() });
58567
+ const typeNodes = data.namespace?.workItemTypes?.nodes;
58568
+ if (!typeNodes?.length) {
58569
+ throw new Error(`Work item type '${typeName}' not found in project`);
58570
+ }
58571
+ const typeNode = typeNodes[0];
58572
+ const statusWidget = findWidget(typeNode.widgetDefinitions || [], "WorkItemWidgetDefinitionStatus");
58573
+ const statuses = statusWidget?.allowedStatuses || [];
58574
+ const hierarchyWidget = findWidget(typeNode.widgetDefinitions || [], "WorkItemWidgetDefinitionHierarchy");
58575
+ const result = {
58576
+ work_item_type: typeNode.name,
58577
+ statuses_available: statuses.length > 0,
58578
+ statuses
58579
+ };
58580
+ const conversionTypes = typeNode.supportedConversionTypes || [];
58581
+ if (conversionTypes.length > 0)
58582
+ result.supported_conversion_types = conversionTypes.map((t) => t.name);
58583
+ const childTypes = hierarchyWidget?.allowedChildTypes?.nodes || [];
58584
+ const parentTypes = hierarchyWidget?.allowedParentTypes?.nodes || [];
58585
+ if (childTypes.length > 0)
58586
+ result.allowed_child_types = childTypes.map((t) => t.name);
58587
+ if (parentTypes.length > 0)
58588
+ result.allowed_parent_types = parentTypes.map((t) => t.name);
58589
+ return result;
58590
+ }
58591
+ async function listCustomFieldDefinitions(projectId, workItemType = "issue") {
58592
+ const projectPath = await resolveProjectPath(projectId);
58593
+ const typeName = WORK_ITEM_TYPE_NAMES[workItemType] || "Issue";
58594
+ const data = await defaultClient.graphql(`query($path: ID!, $typeName: IssueType) {
58595
+ namespace(fullPath: $path) {
58596
+ workItemTypes(name: $typeName) {
58597
+ nodes {
58598
+ id name
58599
+ widgetDefinitions {
58600
+ __typename
58601
+ ... on WorkItemWidgetDefinitionCustomFields {
58602
+ customFieldValues {
58603
+ customField { id name fieldType selectOptions { id value } workItemTypes { id name } }
58604
+ }
58605
+ }
58606
+ }
58607
+ }
58608
+ }
58609
+ }
58610
+ }`, { path: projectPath, typeName: typeName.replace(/ /g, "_").toUpperCase() });
58611
+ const typeNodes = data.namespace?.workItemTypes?.nodes;
58612
+ if (!typeNodes?.length) {
58613
+ throw new Error(`Work item type '${typeName}' not found in project`);
58614
+ }
58615
+ const typeNode = typeNodes[0];
58616
+ const cfWidget = findWidget(typeNode.widgetDefinitions || [], "WorkItemWidgetDefinitionCustomFields");
58617
+ const fields = (cfWidget?.customFieldValues || []).map((cfv) => {
58618
+ const cf = cfv.customField;
58619
+ const field = { id: cf?.id, name: cf?.name, type: cf?.fieldType };
58620
+ const opts = cf?.selectOptions || [];
58621
+ if (opts.length > 0)
58622
+ field.selectOptions = opts;
58623
+ const types = (cf?.workItemTypes || []).map((t) => t.name);
58624
+ if (types.length > 0)
58625
+ field.workItemTypes = types;
58626
+ return field;
58627
+ });
58628
+ return { work_item_type: typeNode.name, custom_fields: fields };
58629
+ }
58630
+ async function moveWorkItem(projectId, iid, targetProjectId) {
58631
+ const projectPath = await resolveProjectPath(projectId);
58632
+ const targetPath = await resolveProjectPath(targetProjectId);
58633
+ const data = await defaultClient.graphql(`mutation($projectPath: ID!, $iid: String!, $targetProjectPath: ID!) {
58634
+ issueMove(input: { projectPath: $projectPath, iid: $iid, targetProjectPath: $targetProjectPath }) {
58635
+ issue { id iid webUrl }
58636
+ errors
58637
+ }
58638
+ }`, { projectPath, iid: String(iid), targetProjectPath: targetPath });
58639
+ if (data.issueMove.errors?.length > 0) {
58640
+ throw new Error(`Failed to move work item: ${data.issueMove.errors.join(", ")}`);
58641
+ }
58642
+ return data.issueMove.issue;
58643
+ }
58644
+ async function listWorkItemNotes(projectId, iid, options = {}) {
58645
+ const projectPath = await resolveProjectPath(projectId);
58646
+ const data = await defaultClient.graphql(`query($path: ID!, $iid: String!, $pageSize: Int, $after: String, $sort: WorkItemDiscussionsSort) {
58647
+ namespace(fullPath: $path) {
58648
+ workItem(iid: $iid) {
58649
+ id
58650
+ widgets(onlyTypes: [NOTES]) {
58651
+ ... on WorkItemWidgetNotes {
58652
+ discussionLocked
58653
+ discussions(first: $pageSize, after: $after, filter: ALL_NOTES, sort: $sort) {
58654
+ pageInfo { hasNextPage endCursor }
58655
+ nodes {
58656
+ id resolved resolvable
58657
+ notes {
58658
+ nodes { id body system internal createdAt lastEditedAt author { username } }
58659
+ }
58660
+ }
58661
+ }
58662
+ }
58663
+ }
58664
+ }
58665
+ }
58666
+ }`, {
58667
+ path: projectPath,
58668
+ iid: String(iid),
58669
+ pageSize: options.page_size || 20,
58670
+ after: options.after || null,
58671
+ sort: options.sort || "CREATED_ASC"
58672
+ });
58673
+ const workItem = data.namespace?.workItem;
58674
+ if (!workItem) {
58675
+ throw new Error(`Work item #${iid} not found in project ${projectPath}`);
58676
+ }
58677
+ const notesWidget = (workItem.widgets || []).find((w) => w.discussions);
58678
+ const discussions = notesWidget?.discussions;
58679
+ const items = (discussions?.nodes || []).map((d) => {
58680
+ const notes = (d.notes?.nodes || []).map((n) => {
58681
+ const note = {
58682
+ id: n.id,
58683
+ author: n.author?.username,
58684
+ body: n.body,
58685
+ createdAt: n.createdAt
58686
+ };
58687
+ if (n.system)
58688
+ note.system = true;
58689
+ if (n.internal)
58690
+ note.internal = true;
58691
+ if (n.lastEditedAt)
58692
+ note.lastEditedAt = n.lastEditedAt;
58693
+ return note;
58694
+ });
58695
+ const discussion = { id: d.id, notes };
58696
+ if (d.resolved)
58697
+ discussion.resolved = true;
58698
+ if (d.resolvable)
58699
+ discussion.resolvable = true;
58700
+ return discussion;
58701
+ });
58702
+ return { discussions: items, pageInfo: discussions?.pageInfo || {} };
58703
+ }
58704
+ async function createWorkItemNote(projectId, iid, body, options = {}) {
58705
+ const { workItemGID } = await resolveWorkItemGID(projectId, iid);
58706
+ const varDefs = ["$notableId: NotableID!", "$body: String!"];
58707
+ const inputParts = ["notableId: $notableId", "body: $body"];
58708
+ const variables = { notableId: workItemGID, body };
58709
+ if (options.internal) {
58710
+ varDefs.push("$internal: Boolean");
58711
+ inputParts.push("internal: $internal");
58712
+ variables.internal = true;
58713
+ }
58714
+ if (options.discussion_id) {
58715
+ varDefs.push("$discussionId: DiscussionID");
58716
+ inputParts.push("discussionId: $discussionId");
58717
+ variables.discussionId = options.discussion_id;
58718
+ }
58719
+ const data = await defaultClient.graphql(`mutation(${varDefs.join(", ")}) {
58720
+ createNote(input: { ${inputParts.join(", ")} }) {
58721
+ note { id body discussion { id } }
58722
+ errors
58723
+ }
58724
+ }`, variables);
58725
+ if (data.createNote.errors?.length > 0) {
58726
+ throw new Error(`Failed to create note: ${data.createNote.errors.join(", ")}`);
58727
+ }
58728
+ return data.createNote.note;
58729
+ }
58730
+ async function getTimelineEvents(projectId, incidentIid) {
58731
+ const { workItemGID, projectPath } = await resolveWorkItemGID(projectId, incidentIid);
58732
+ const incidentGID = workItemGID.replace("/WorkItem/", "/Issue/");
58733
+ const data = await defaultClient.graphql(`query($fullPath: ID!, $incidentId: IssueID!) {
58734
+ project(fullPath: $fullPath) {
58735
+ incidentManagementTimelineEvents(incidentId: $incidentId) {
58736
+ nodes { id note noteHtml action occurredAt createdAt timelineEventTags { nodes { id name } } }
58737
+ }
58738
+ }
58739
+ }`, { fullPath: projectPath, incidentId: incidentGID });
58740
+ const events = data.project?.incidentManagementTimelineEvents?.nodes || [];
58741
+ return events.map((e) => {
58742
+ const event = {
58743
+ id: e.id,
58744
+ note: e.note,
58745
+ action: e.action,
58746
+ occurredAt: e.occurredAt,
58747
+ createdAt: e.createdAt
58748
+ };
58749
+ if (e.noteHtml)
58750
+ event.noteHtml = e.noteHtml;
58751
+ const tags = (e.timelineEventTags?.nodes || []).map((t) => t.name);
58752
+ if (tags.length > 0)
58753
+ event.tags = tags;
58754
+ return event;
58755
+ });
58756
+ }
58757
+ async function createTimelineEvent(projectId, incidentIid, note, occurredAt, tagNames) {
58758
+ const { workItemGID } = await resolveWorkItemGID(projectId, incidentIid);
58759
+ const incidentGID = workItemGID.replace("/WorkItem/", "/Issue/");
58760
+ const input = { incidentId: incidentGID, note, occurredAt };
58761
+ if (tagNames?.length)
58762
+ input.timelineEventTagNames = tagNames;
58763
+ const data = await defaultClient.graphql(`mutation CreateTimelineEvent($input: TimelineEventCreateInput!) {
58764
+ timelineEventCreate(input: $input) {
58765
+ timelineEvent { id note noteHtml action occurredAt createdAt timelineEventTags { nodes { id name } } }
58766
+ errors
58767
+ }
58768
+ }`, { input });
58769
+ if (data.timelineEventCreate.errors?.length > 0) {
58770
+ throw new Error(`Failed to create timeline event: ${data.timelineEventCreate.errors.join(", ")}`);
58771
+ }
58772
+ const e = data.timelineEventCreate.timelineEvent;
58773
+ const result = {
58774
+ id: e.id,
58775
+ note: e.note,
58776
+ action: e.action,
58777
+ occurredAt: e.occurredAt,
58778
+ createdAt: e.createdAt
58779
+ };
58780
+ if (e.noteHtml)
58781
+ result.noteHtml = e.noteHtml;
58782
+ const tags = (e.timelineEventTags?.nodes || []).map((t) => t.name);
58783
+ if (tags.length > 0)
58784
+ result.tags = tags;
58785
+ return result;
58786
+ }
58787
+ function jsonResponse(data) {
58788
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
58789
+ }
58790
+ function registerWorkItemTools(server, logger2) {
58791
+ logger2.debug("Registering work item tools");
58792
+ const tools = new Map;
58793
+ const t1 = server.registerTool("get_work_item", {
58794
+ title: "Get Work Item",
58795
+ description: "Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets.",
58796
+ inputSchema: {
58797
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58798
+ iid: exports_external.coerce.number().describe("The internal ID (IID) of the work item")
58799
+ }
58800
+ }, async (params) => {
58801
+ const args = GetWorkItemSchema.parse(params);
58802
+ return jsonResponse(await getWorkItem(args.project_id, args.iid));
58803
+ });
58804
+ t1.disable();
58805
+ tools.set("get_work_item", t1);
58806
+ const t2 = server.registerTool("list_work_items", {
58807
+ title: "List Work Items",
58808
+ description: "List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info.",
58809
+ inputSchema: {
58810
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58811
+ types: exports_external.array(workItemTypeEnum).optional().describe("Filter by work item types"),
58812
+ state: exports_external.enum(["opened", "closed"]).optional().describe("Filter by state"),
58813
+ search: exports_external.string().optional().describe("Search in title and description"),
58814
+ assignee_usernames: exports_external.array(exports_external.string()).optional().describe("Filter by assignee usernames"),
58815
+ label_names: exports_external.array(exports_external.string()).optional().describe("Filter by label names"),
58816
+ first: exports_external.coerce.number().optional().default(20).describe("Number of items to return (max 100)"),
58817
+ after: exports_external.string().optional().describe("Cursor for pagination")
58818
+ }
58819
+ }, async (params) => {
58820
+ const args = ListWorkItemsSchema.parse(params);
58821
+ const { project_id, ...opts } = args;
58822
+ return jsonResponse(await listWorkItems(project_id, opts));
58823
+ });
58824
+ t2.disable();
58825
+ tools.set("list_work_items", t2);
58826
+ const t3 = server.registerTool("create_work_item", {
58827
+ title: "Create Work Item",
58828
+ description: "Create a new work item (issue, task, incident, test_case, epic, key_result, objective, requirement, ticket). Supports setting title, description, labels, assignees, weight, parent, health status, start/due dates, milestone, and confidentiality.",
58829
+ inputSchema: {
58830
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58831
+ title: exports_external.string().describe("Title of the work item"),
58832
+ type: workItemTypeEnum.optional().default("issue").describe("Type of work item"),
58833
+ description: exports_external.string().optional().describe("Description (Markdown supported)"),
58834
+ labels: exports_external.array(exports_external.string()).optional().describe("Label names to assign"),
58835
+ assignee_usernames: exports_external.array(exports_external.string()).optional().describe("Usernames to assign"),
58836
+ parent_iid: exports_external.coerce.number().optional().describe("IID of parent work item"),
58837
+ weight: exports_external.coerce.number().optional().describe("Weight"),
58838
+ health_status: exports_external.enum(["onTrack", "needsAttention", "atRisk"]).optional().describe("Health status"),
58839
+ start_date: exports_external.string().optional().describe("Start date (YYYY-MM-DD)"),
58840
+ due_date: exports_external.string().optional().describe("Due date (YYYY-MM-DD)"),
58841
+ milestone_id: exports_external.string().optional().describe("Milestone ID"),
58842
+ iteration_id: exports_external.string().optional().describe("Iteration ID"),
58843
+ confidential: exports_external.coerce.boolean().optional().describe("Set confidentiality")
58844
+ }
58845
+ }, async (params) => {
58846
+ const args = CreateWorkItemSchema.parse(params);
58847
+ const { project_id, ...opts } = args;
58848
+ return jsonResponse(await createWorkItem(project_id, opts));
58849
+ });
58850
+ t3.disable();
58851
+ tools.set("create_work_item", t3);
58852
+ const t4 = server.registerTool("update_work_item", {
58853
+ title: "Update Work Item",
58854
+ description: "Update a work item. Can modify title, description, labels, assignees, weight, state, status, parent hierarchy, children, health status, start/due dates, milestone, confidentiality, linked items, and custom fields.",
58855
+ inputSchema: {
58856
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58857
+ iid: exports_external.coerce.number().describe("The internal ID (IID) of the work item"),
58858
+ title: exports_external.string().optional().describe("New title"),
58859
+ description: exports_external.string().optional().describe("New description"),
58860
+ add_labels: exports_external.array(exports_external.string()).optional().describe("Label names to add"),
58861
+ remove_labels: exports_external.array(exports_external.string()).optional().describe("Label names to remove"),
58862
+ assignee_usernames: exports_external.array(exports_external.string()).optional().describe("Set assignees by username"),
58863
+ state_event: exports_external.enum(["close", "reopen"]).optional().describe("Close or reopen"),
58864
+ weight: exports_external.coerce.number().optional().describe("Set weight"),
58865
+ status: exports_external.string().optional().describe("Set status by ID"),
58866
+ parent_iid: exports_external.coerce.number().optional().describe("Set parent by IID"),
58867
+ parent_project_id: exports_external.coerce.string().optional().describe("Parent project ID"),
58868
+ remove_parent: exports_external.coerce.boolean().optional().describe("Remove parent"),
58869
+ health_status: exports_external.enum(["onTrack", "needsAttention", "atRisk"]).optional().describe("Health status"),
58870
+ start_date: exports_external.string().optional().describe("Start date (YYYY-MM-DD)"),
58871
+ due_date: exports_external.string().optional().describe("Due date (YYYY-MM-DD)"),
58872
+ milestone_id: exports_external.string().optional().describe("Milestone ID"),
58873
+ iteration_id: exports_external.string().optional().describe("Iteration ID"),
58874
+ confidential: exports_external.coerce.boolean().optional().describe("Confidentiality"),
58875
+ severity: exports_external.enum(["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional().describe("Incident severity"),
58876
+ escalation_status: exports_external.enum(["TRIGGERED", "ACKNOWLEDGED", "RESOLVED", "IGNORED"]).optional().describe("Incident escalation status")
58877
+ }
58878
+ }, async (params) => {
58879
+ const args = UpdateWorkItemSchema.parse(params);
58880
+ const { project_id, iid: wiIid, ...opts } = args;
58881
+ return jsonResponse(await updateWorkItem(project_id, wiIid, opts));
58882
+ });
58883
+ t4.disable();
58884
+ tools.set("update_work_item", t4);
58885
+ const t5 = server.registerTool("convert_work_item_type", {
58886
+ title: "Convert Work Item Type",
58887
+ description: "Convert a work item to a different type (e.g. issue to task, task to incident).",
58888
+ inputSchema: {
58889
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58890
+ iid: exports_external.coerce.number().describe("The internal ID of the work item"),
58891
+ new_type: workItemTypeEnum.describe("The target work item type")
58892
+ }
58893
+ }, async (params) => {
58894
+ const args = ConvertWorkItemTypeSchema.parse(params);
58895
+ return jsonResponse(await convertWorkItemType(args.project_id, args.iid, args.new_type));
58896
+ });
58897
+ t5.disable();
58898
+ tools.set("convert_work_item_type", t5);
58899
+ const t6 = server.registerTool("list_work_item_statuses", {
58900
+ title: "List Work Item Statuses",
58901
+ description: "List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses.",
58902
+ inputSchema: {
58903
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58904
+ work_item_type: workItemTypeEnum.optional().default("issue").describe("Work item type")
58905
+ }
58906
+ }, async (params) => {
58907
+ const args = ListWorkItemStatusesSchema.parse(params);
58908
+ return jsonResponse(await listWorkItemStatuses(args.project_id, args.work_item_type));
58909
+ });
58910
+ t6.disable();
58911
+ tools.set("list_work_item_statuses", t6);
58912
+ const t7 = server.registerTool("list_custom_field_definitions", {
58913
+ title: "List Custom Field Definitions",
58914
+ description: "List available custom field definitions for a work item type in a project. Returns field names, types, and IDs needed for setting custom fields via update_work_item.",
58915
+ inputSchema: {
58916
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58917
+ work_item_type: workItemTypeEnum.optional().default("issue").describe("Work item type")
58918
+ }
58919
+ }, async (params) => {
58920
+ const args = ListCustomFieldDefinitionsSchema.parse(params);
58921
+ return jsonResponse(await listCustomFieldDefinitions(args.project_id, args.work_item_type));
58922
+ });
58923
+ t7.disable();
58924
+ tools.set("list_custom_field_definitions", t7);
58925
+ const t8 = server.registerTool("move_work_item", {
58926
+ title: "Move Work Item",
58927
+ description: "Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation.",
58928
+ inputSchema: {
58929
+ project_id: exports_external.coerce.string().describe("Source project ID or path"),
58930
+ iid: exports_external.coerce.number().describe("The internal ID of the work item"),
58931
+ target_project_id: exports_external.coerce.string().describe("Target project ID or path")
58932
+ }
58933
+ }, async (params) => {
58934
+ const args = MoveWorkItemSchema.parse(params);
58935
+ return jsonResponse(await moveWorkItem(args.project_id, args.iid, args.target_project_id));
58936
+ });
58937
+ t8.disable();
58938
+ tools.set("move_work_item", t8);
58939
+ const t9 = server.registerTool("list_work_item_notes", {
58940
+ title: "List Work Item Notes",
58941
+ description: "List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags.",
58942
+ inputSchema: {
58943
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58944
+ iid: exports_external.coerce.number().describe("The internal ID of the work item"),
58945
+ page_size: exports_external.coerce.number().optional().default(20).describe("Number of discussions to return"),
58946
+ after: exports_external.string().optional().describe("Cursor for pagination"),
58947
+ sort: exports_external.enum(["CREATED_ASC", "CREATED_DESC"]).optional().default("CREATED_ASC").describe("Sort order")
58948
+ }
58949
+ }, async (params) => {
58950
+ const args = ListWorkItemNotesSchema.parse(params);
58951
+ return jsonResponse(await listWorkItemNotes(args.project_id, args.iid, {
58952
+ page_size: args.page_size,
58953
+ after: args.after,
58954
+ sort: args.sort
58955
+ }));
58956
+ });
58957
+ t9.disable();
58958
+ tools.set("list_work_item_notes", t9);
58959
+ const t10 = server.registerTool("create_work_item_note", {
58960
+ title: "Create Work Item Note",
58961
+ description: "Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies.",
58962
+ inputSchema: {
58963
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58964
+ iid: exports_external.coerce.number().describe("The internal ID of the work item"),
58965
+ body: exports_external.string().describe("Note body (Markdown supported)"),
58966
+ internal: exports_external.coerce.boolean().optional().default(false).describe("Internal note"),
58967
+ discussion_id: exports_external.string().optional().describe("Discussion ID for threaded reply")
58968
+ }
58969
+ }, async (params) => {
58970
+ const args = CreateWorkItemNoteSchema.parse(params);
58971
+ return jsonResponse(await createWorkItemNote(args.project_id, args.iid, args.body, {
58972
+ internal: args.internal,
58973
+ discussion_id: args.discussion_id
58974
+ }));
58975
+ });
58976
+ t10.disable();
58977
+ tools.set("create_work_item_note", t10);
58978
+ const t11 = server.registerTool("get_timeline_events", {
58979
+ title: "Get Timeline Events",
58980
+ description: "List timeline events for an incident. Returns chronological events with notes, timestamps, and tags.",
58981
+ inputSchema: {
58982
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58983
+ incident_iid: exports_external.coerce.number().describe("The internal ID of the incident")
58984
+ }
58985
+ }, async (params) => {
58986
+ const args = GetTimelineEventsSchema.parse(params);
58987
+ return jsonResponse(await getTimelineEvents(args.project_id, args.incident_iid));
58988
+ });
58989
+ t11.disable();
58990
+ tools.set("get_timeline_events", t11);
58991
+ const t12 = server.registerTool("create_timeline_event", {
58992
+ title: "Create Timeline Event",
58993
+ description: "Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'.",
58994
+ inputSchema: {
58995
+ project_id: exports_external.coerce.string().describe("Project ID or URL-encoded path"),
58996
+ incident_iid: exports_external.coerce.number().describe("The internal ID of the incident"),
58997
+ note: exports_external.string().describe("Description of the timeline event (Markdown supported)"),
58998
+ occurred_at: exports_external.string().describe("When the event occurred (ISO 8601 format)"),
58999
+ tag_names: exports_external.array(exports_external.enum([
59000
+ "Start time",
59001
+ "End time",
59002
+ "Impact detected",
59003
+ "Response initiated",
59004
+ "Impact mitigated",
59005
+ "Cause identified"
59006
+ ])).optional().describe("Timeline event tags")
59007
+ }
59008
+ }, async (params) => {
59009
+ const args = CreateTimelineEventSchema.parse(params);
59010
+ return jsonResponse(await createTimelineEvent(args.project_id, args.incident_iid, args.note, args.occurred_at, args.tag_names));
59011
+ });
59012
+ t12.disable();
59013
+ tools.set("create_timeline_event", t12);
59014
+ logger2.debug("Work item tools registered", { count: tools.size });
59015
+ return tools;
59016
+ }
57615
59017
  // src/server/index.ts
57616
59018
  function createMcpServer(config3, logger2) {
57617
59019
  const mcpServer = new McpServer({
@@ -57634,6 +59036,7 @@ function createMcpServer(config3, logger2) {
57634
59036
  toolsByCategory.set("search", registerSearchTools(mcpServer, logger2));
57635
59037
  toolsByCategory.set("releases", registerReleaseTools(mcpServer, logger2));
57636
59038
  toolsByCategory.set("webhooks", registerWebhookTools(mcpServer, logger2));
59039
+ toolsByCategory.set("work-items", registerWorkItemTools(mcpServer, logger2));
57637
59040
  toolsByCategory.set("graphql", registerGraphqlTools(mcpServer, logger2));
57638
59041
  if (config3.useGitlabWiki) {
57639
59042
  toolsByCategory.set("wiki", registerWikiTools(mcpServer, logger2));