@aaronsb/jira-cloud-mcp 0.2.7 → 0.3.1

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.
@@ -4,6 +4,10 @@ export class JiraClient {
4
4
  client;
5
5
  agileClient;
6
6
  customFields;
7
+ /** Expose the underlying Version3Client for field discovery and other direct API access */
8
+ get v3Client() {
9
+ return this.client;
10
+ }
7
11
  constructor(config) {
8
12
  const clientConfig = {
9
13
  host: config.host,
@@ -22,8 +26,47 @@ export class JiraClient {
22
26
  storyPoints: config.customFields?.storyPoints ?? 'customfield_10016',
23
27
  };
24
28
  }
25
- async getIssue(issueKey, includeComments = false, includeAttachments = false) {
26
- const fields = [
29
+ /** Standard Jira fields that require ADF format in v3 API */
30
+ static ADF_FIELDS = new Set(['environment']);
31
+ /** Convert any ADF-type fields in customFields from markdown to ADF */
32
+ convertAdfFields(customFields) {
33
+ const result = {};
34
+ for (const [key, value] of Object.entries(customFields)) {
35
+ if (JiraClient.ADF_FIELDS.has(key) && typeof value === 'string') {
36
+ result[key] = TextProcessor.markdownToAdf(value);
37
+ }
38
+ else {
39
+ result[key] = value;
40
+ }
41
+ }
42
+ return result;
43
+ }
44
+ /** Format a custom field value for display */
45
+ formatCustomFieldValue(value) {
46
+ if (value === null || value === undefined)
47
+ return null;
48
+ // Jira option fields return { value: "..." } or { name: "..." }
49
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
50
+ const obj = value;
51
+ if ('value' in obj)
52
+ return obj.value;
53
+ if ('name' in obj)
54
+ return obj.name;
55
+ if ('displayName' in obj)
56
+ return obj.displayName;
57
+ // ADF content — extract text
58
+ if ('content' in obj && obj.type === 'doc')
59
+ return TextProcessor.extractTextFromAdf(obj);
60
+ }
61
+ // Array of options
62
+ if (Array.isArray(value)) {
63
+ return value.map(item => this.formatCustomFieldValue(item));
64
+ }
65
+ return value;
66
+ }
67
+ /** Shared field list for issue queries */
68
+ get issueFields() {
69
+ return [
27
70
  'summary',
28
71
  'description',
29
72
  'parent',
@@ -37,34 +80,69 @@ export class JiraClient {
37
80
  'timeestimate',
38
81
  'issuelinks',
39
82
  ];
83
+ }
84
+ /** Maps a raw Jira API issue to our JiraIssueDetails shape */
85
+ mapIssueFields(issue) {
86
+ const fields = issue.fields ?? issue.fields;
87
+ return {
88
+ key: issue.key,
89
+ summary: fields?.summary,
90
+ description: issue.renderedFields?.description || '',
91
+ parent: fields?.parent?.key || null,
92
+ assignee: fields?.assignee?.displayName || null,
93
+ reporter: fields?.reporter?.displayName || '',
94
+ status: fields?.status?.name || '',
95
+ resolution: fields?.resolution?.name || null,
96
+ dueDate: fields?.duedate || null,
97
+ startDate: fields?.[this.customFields.startDate] || null,
98
+ storyPoints: fields?.[this.customFields.storyPoints] || null,
99
+ timeEstimate: fields?.timeestimate || null,
100
+ issueLinks: (fields?.issuelinks || []).map((link) => ({
101
+ type: link.type?.name || '',
102
+ outward: link.outwardIssue?.key || null,
103
+ inward: link.inwardIssue?.key || null,
104
+ })),
105
+ };
106
+ }
107
+ async getIssue(issueKey, includeComments = false, includeAttachments = false, customFieldMeta) {
108
+ const fields = [...this.issueFields];
40
109
  if (includeAttachments) {
41
110
  fields.push('attachment');
42
111
  }
112
+ // Include discovered custom field IDs in the fetch
113
+ if (customFieldMeta) {
114
+ for (const cf of customFieldMeta) {
115
+ if (!fields.includes(cf.id)) {
116
+ fields.push(cf.id);
117
+ }
118
+ }
119
+ }
43
120
  const params = {
44
121
  issueIdOrKey: issueKey,
45
122
  fields,
46
123
  expand: includeComments ? 'renderedFields,comments' : 'renderedFields'
47
124
  };
48
125
  const issue = await this.client.issues.getIssue(params);
49
- const issueDetails = {
50
- key: issue.key,
51
- summary: issue.fields.summary,
52
- description: issue.renderedFields?.description || '',
53
- parent: issue.fields.parent?.key || null,
54
- assignee: issue.fields.assignee?.displayName || null,
55
- reporter: issue.fields.reporter?.displayName || '',
56
- status: issue.fields.status?.name || '',
57
- resolution: issue.fields.resolution?.name || null,
58
- dueDate: issue.fields.duedate || null,
59
- startDate: issue.fields[this.customFields.startDate] || null,
60
- storyPoints: issue.fields[this.customFields.storyPoints] || null,
61
- timeEstimate: issue.fields.timeestimate || null,
62
- issueLinks: (issue.fields.issuelinks || []).map(link => ({
63
- type: link.type?.name || '',
64
- outward: link.outwardIssue?.key || null,
65
- inward: link.inwardIssue?.key || null,
66
- })),
67
- };
126
+ const issueDetails = this.mapIssueFields(issue);
127
+ // Extract custom field values using catalog metadata
128
+ if (customFieldMeta) {
129
+ const rawFields = issue.fields;
130
+ const customValues = [];
131
+ for (const cf of customFieldMeta) {
132
+ const value = rawFields[cf.id];
133
+ if (value !== undefined && value !== null) {
134
+ customValues.push({
135
+ name: cf.name,
136
+ value: this.formatCustomFieldValue(value),
137
+ type: cf.type,
138
+ description: cf.description,
139
+ });
140
+ }
141
+ }
142
+ if (customValues.length > 0) {
143
+ issueDetails.customFieldValues = customValues;
144
+ }
145
+ }
68
146
  if (includeComments && issue.fields.comment?.comments) {
69
147
  issueDetails.comments = issue.fields.comment.comments
70
148
  .filter(comment => comment.id &&
@@ -130,40 +208,10 @@ export class JiraClient {
130
208
  }
131
209
  const searchResults = await this.client.issueSearch.searchForIssuesUsingJql({
132
210
  jql: filter.jql,
133
- fields: [
134
- 'summary',
135
- 'description',
136
- 'assignee',
137
- 'reporter',
138
- 'status',
139
- 'resolution',
140
- 'duedate',
141
- this.customFields.startDate,
142
- this.customFields.storyPoints,
143
- 'timeestimate',
144
- 'issuelinks',
145
- ],
211
+ fields: this.issueFields,
146
212
  expand: 'renderedFields'
147
213
  });
148
- return (searchResults.issues || []).map(issue => ({
149
- key: issue.key,
150
- summary: issue.fields.summary,
151
- description: issue.renderedFields?.description || '',
152
- parent: issue.fields.parent?.key || null,
153
- assignee: issue.fields.assignee?.displayName || null,
154
- reporter: issue.fields.reporter?.displayName || '',
155
- status: issue.fields.status?.name || '',
156
- resolution: issue.fields.resolution?.name || null,
157
- dueDate: issue.fields.duedate || null,
158
- startDate: issue.fields[this.customFields.startDate] || null,
159
- storyPoints: issue.fields[this.customFields.storyPoints] || null,
160
- timeEstimate: issue.fields.timeestimate || null,
161
- issueLinks: (issue.fields.issuelinks || []).map(link => ({
162
- type: link.type?.name || '',
163
- outward: link.outwardIssue?.key || null,
164
- inward: link.inwardIssue?.key || null,
165
- })),
166
- }));
214
+ return (searchResults.issues || []).map(issue => this.mapIssueFields(issue));
167
215
  }
168
216
  async updateIssue(params) {
169
217
  const fields = {};
@@ -184,7 +232,7 @@ export class JiraClient {
184
232
  if (params.labels)
185
233
  fields.labels = params.labels;
186
234
  if (params.customFields) {
187
- Object.assign(fields, params.customFields);
235
+ Object.assign(fields, this.convertAdfFields(params.customFields));
188
236
  }
189
237
  await this.client.issues.editIssue({
190
238
  issueIdOrKey: params.issueKey,
@@ -206,41 +254,10 @@ export class JiraClient {
206
254
  const searchResults = await this.client.issueSearch.searchForIssuesUsingJqlEnhancedSearch({
207
255
  jql: cleanJql,
208
256
  maxResults: Math.min(maxResults, 100),
209
- fields: [
210
- 'summary',
211
- 'description',
212
- 'assignee',
213
- 'reporter',
214
- 'status',
215
- 'resolution',
216
- 'duedate',
217
- 'parent',
218
- this.customFields.startDate,
219
- this.customFields.storyPoints,
220
- 'timeestimate',
221
- 'issuelinks',
222
- ],
257
+ fields: this.issueFields,
223
258
  expand: 'renderedFields',
224
259
  });
225
- const issues = (searchResults.issues || []).map(issue => ({
226
- key: issue.key,
227
- summary: issue.fields?.summary,
228
- description: issue.renderedFields?.description || '',
229
- parent: issue.fields?.parent?.key || null,
230
- assignee: issue.fields?.assignee?.displayName || null,
231
- reporter: issue.fields?.reporter?.displayName || '',
232
- status: issue.fields?.status?.name || '',
233
- resolution: issue.fields?.resolution?.name || null,
234
- dueDate: issue.fields?.duedate || null,
235
- startDate: issue.fields?.[this.customFields.startDate] || null,
236
- storyPoints: issue.fields?.[this.customFields.storyPoints] || null,
237
- timeEstimate: issue.fields?.timeestimate || null,
238
- issueLinks: (issue.fields?.issuelinks || []).map(link => ({
239
- type: link.type?.name || '',
240
- outward: link.outwardIssue?.key || null,
241
- inward: link.inwardIssue?.key || null,
242
- })),
243
- }));
260
+ const issues = (searchResults.issues || []).map(issue => this.mapIssueFields(issue));
244
261
  // Note: Enhanced search API uses token-based pagination, not offset-based
245
262
  // The total count is not available in the new API
246
263
  const hasMore = !!searchResults.nextPageToken;
@@ -684,6 +701,20 @@ export class JiraClient {
684
701
  url: project.self || ''
685
702
  }));
686
703
  }
704
+ async deleteIssue(issueKey) {
705
+ console.error(`Deleting issue ${issueKey}...`);
706
+ await this.client.issues.deleteIssue({ issueIdOrKey: issueKey });
707
+ }
708
+ async moveIssue(issueKey, targetProjectKey, targetIssueType) {
709
+ console.error(`Moving issue ${issueKey} to ${targetProjectKey} as ${targetIssueType}...`);
710
+ await this.client.issues.editIssue({
711
+ issueIdOrKey: issueKey,
712
+ fields: {
713
+ project: { key: targetProjectKey },
714
+ issuetype: { name: targetIssueType },
715
+ },
716
+ });
717
+ }
687
718
  async createIssue(params) {
688
719
  const fields = {
689
720
  project: { key: params.projectKey },
@@ -700,7 +731,7 @@ export class JiraClient {
700
731
  if (params.labels)
701
732
  fields.labels = params.labels;
702
733
  if (params.customFields) {
703
- Object.assign(fields, params.customFields);
734
+ Object.assign(fields, this.convertAdfFields(params.customFields));
704
735
  }
705
736
  const response = await this.client.issues.createIssue({ fields });
706
737
  return { key: response.key };