@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.
- package/README.md +22 -30
- package/build/client/field-discovery.js +346 -0
- package/build/client/field-type-map.js +106 -0
- package/build/client/jira-client.js +119 -88
- package/build/docs/tool-documentation.js +481 -0
- package/build/handlers/board-handlers.js +10 -153
- package/build/handlers/filter-handlers.js +7 -98
- package/build/handlers/issue-handlers.js +148 -75
- package/build/handlers/project-handlers.js +11 -154
- package/build/handlers/queue-handler.js +252 -0
- package/build/handlers/resource-handlers.js +94 -19
- package/build/handlers/sprint-handlers.js +9 -94
- package/build/handlers/tool-resource-handlers.js +16 -1165
- package/build/index.js +59 -51
- package/build/mcp/markdown-renderer.js +11 -0
- package/build/schemas/tool-schemas.js +98 -197
- package/build/utils/bulk-operation-guard.js +74 -0
- package/build/utils/next-steps.js +124 -0
- package/build/utils/normalize-args.js +12 -0
- package/build/utils/text-processing.js +5 -1
- package/package.json +6 -4
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 };
|