@mcpio/jira 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +37 -0
- package/index.js +797 -261
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npx tsc:*)",
|
|
5
|
+
"Bash(/Users/wince/projects/ridebook-front/node_modules/.bin/tsc:*)",
|
|
6
|
+
"Bash(/Users/wince/projects/ridebook-backend/node_modules/.bin/tsc:*)",
|
|
7
|
+
"Bash(npm run build:*)",
|
|
8
|
+
"WebFetch(domain:help.mikrotik.com)",
|
|
9
|
+
"Bash(curl:*)",
|
|
10
|
+
"Bash(python3:*)",
|
|
11
|
+
"Bash(npm install:*)",
|
|
12
|
+
"Bash(for id in '*4' '*5' '*6' '*7' '*8' '*9' '*A' '*B' '*C' '*D' '*E' '*F' '*10')",
|
|
13
|
+
"Bash(do)",
|
|
14
|
+
"Bash(done)",
|
|
15
|
+
"Bash(for id in '*80000003' '*80000004' '*80000005' '*80000006')",
|
|
16
|
+
"Bash(for id in '*400' '*401')",
|
|
17
|
+
"Bash(printf:*)",
|
|
18
|
+
"Bash(npx vite build:*)",
|
|
19
|
+
"Bash(git add:*)",
|
|
20
|
+
"Bash(git commit -m \"$\\(cat <<''EOF''\nRouter Panel — MikroTik dual-ISP management with PCC load balancing\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
21
|
+
"Bash(git remote add:*)",
|
|
22
|
+
"Bash(git push:*)",
|
|
23
|
+
"Bash(git commit:*)",
|
|
24
|
+
"Bash(lsof:*)",
|
|
25
|
+
"Bash(kill:*)",
|
|
26
|
+
"Bash(node:*)",
|
|
27
|
+
"Bash(ping:*)",
|
|
28
|
+
"WebFetch(domain:developer.atlassian.com)",
|
|
29
|
+
"WebFetch(domain:unpkg.com)",
|
|
30
|
+
"WebFetch(domain:docs.atlassian.com)",
|
|
31
|
+
"WebFetch(domain:dac-static.atlassian.com)",
|
|
32
|
+
"WebFetch(domain:community.developer.atlassian.com)",
|
|
33
|
+
"WebFetch(domain:github.com)",
|
|
34
|
+
"Bash(npm publish:*)"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
package/index.js
CHANGED
|
@@ -14,21 +14,14 @@ import * as dotenv from 'dotenv';
|
|
|
14
14
|
dotenv.config();
|
|
15
15
|
|
|
16
16
|
function getRequiredEnv(name, fallback = null) {
|
|
17
|
-
const value = process.env[name]
|
|
18
|
-
if (
|
|
19
|
-
|
|
17
|
+
const value = process.env[name];
|
|
18
|
+
if (value !== undefined && value !== '') {
|
|
19
|
+
return value;
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const JIRA_EMAIL = getRequiredEnv('JIRA_EMAIL');
|
|
26
|
-
const JIRA_API_TOKEN = getRequiredEnv('JIRA_API_TOKEN');
|
|
27
|
-
const JIRA_PROJECT_KEY = process.env.JIRA_PROJECT_KEY || 'PROJ';
|
|
28
|
-
const STORY_POINTS_FIELD = process.env.JIRA_STORY_POINTS_FIELD || 'customfield_10016';
|
|
29
|
-
|
|
30
|
-
if (!JIRA_URL.startsWith('https://')) {
|
|
31
|
-
throw new Error('JIRA_HOST must use HTTPS protocol for security');
|
|
21
|
+
if (fallback !== null && fallback !== undefined && fallback !== '') {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Required environment variable ${name} is not set. Please check your .env file.`);
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
function validateIssueKey(key) {
|
|
@@ -41,6 +34,16 @@ function validateIssueKey(key) {
|
|
|
41
34
|
return key;
|
|
42
35
|
}
|
|
43
36
|
|
|
37
|
+
function validateProjectKey(key) {
|
|
38
|
+
if (!key || typeof key !== 'string') {
|
|
39
|
+
throw new Error('Invalid project key: must be a string');
|
|
40
|
+
}
|
|
41
|
+
if (!/^[A-Z][A-Z0-9_]{1,9}$/.test(key)) {
|
|
42
|
+
throw new Error(`Invalid project key format: ${key}. Expected 2-10 uppercase alphanumeric characters`);
|
|
43
|
+
}
|
|
44
|
+
return key;
|
|
45
|
+
}
|
|
46
|
+
|
|
44
47
|
function validateJQL(jql) {
|
|
45
48
|
if (!jql || typeof jql !== 'string') {
|
|
46
49
|
throw new Error('Invalid JQL query: must be a string');
|
|
@@ -61,6 +64,58 @@ function sanitizeString(str, maxLength = 1000, fieldName = 'input') {
|
|
|
61
64
|
return str.trim();
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
function validateSafeParam(str, fieldName, maxLength = 100) {
|
|
68
|
+
if (!str || typeof str !== 'string') {
|
|
69
|
+
throw new Error(`Invalid ${fieldName}: must be a non-empty string`);
|
|
70
|
+
}
|
|
71
|
+
if (str.length > maxLength) {
|
|
72
|
+
throw new Error(`${fieldName} exceeds maximum length of ${maxLength} characters`);
|
|
73
|
+
}
|
|
74
|
+
if (/[\/\\]/.test(str)) {
|
|
75
|
+
throw new Error(`Invalid ${fieldName}: contains unsafe characters`);
|
|
76
|
+
}
|
|
77
|
+
return str.trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateMaxResults(maxResults) {
|
|
81
|
+
if (typeof maxResults !== 'number' || !Number.isInteger(maxResults) || maxResults < 1) {
|
|
82
|
+
throw new Error('maxResults must be a positive integer');
|
|
83
|
+
}
|
|
84
|
+
return Math.min(maxResults, 100);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateStoryPoints(points) {
|
|
88
|
+
if (typeof points !== 'number' || points < 0 || points > 1000) {
|
|
89
|
+
throw new Error('Story points must be a number between 0 and 1000');
|
|
90
|
+
}
|
|
91
|
+
return points;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateLabels(labels) {
|
|
95
|
+
if (!Array.isArray(labels)) {
|
|
96
|
+
throw new Error('Labels must be an array');
|
|
97
|
+
}
|
|
98
|
+
return labels.map((label, index) => {
|
|
99
|
+
if (typeof label !== 'string') {
|
|
100
|
+
throw new Error(`Label at index ${index} must be a string`);
|
|
101
|
+
}
|
|
102
|
+
if (label.length > 255) {
|
|
103
|
+
throw new Error(`Label at index ${index} exceeds maximum length of 255 characters`);
|
|
104
|
+
}
|
|
105
|
+
return label;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const JIRA_URL = getRequiredEnv('JIRA_HOST', process.env.JIRA_URL);
|
|
110
|
+
const JIRA_EMAIL = getRequiredEnv('JIRA_EMAIL');
|
|
111
|
+
const JIRA_API_TOKEN = getRequiredEnv('JIRA_API_TOKEN');
|
|
112
|
+
const JIRA_PROJECT_KEY = validateProjectKey(process.env.JIRA_PROJECT_KEY || 'PROJ');
|
|
113
|
+
const STORY_POINTS_FIELD = process.env.JIRA_STORY_POINTS_FIELD || 'customfield_10016';
|
|
114
|
+
|
|
115
|
+
if (!JIRA_URL.startsWith('https://')) {
|
|
116
|
+
throw new Error('JIRA_HOST must use HTTPS protocol for security');
|
|
117
|
+
}
|
|
118
|
+
|
|
64
119
|
function createSuccessResponse(data) {
|
|
65
120
|
return {
|
|
66
121
|
content: [{
|
|
@@ -77,13 +132,22 @@ function createIssueUrl(issueKey) {
|
|
|
77
132
|
function handleError(error) {
|
|
78
133
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
79
134
|
|
|
135
|
+
const jiraErrors = error.response?.data?.errorMessages;
|
|
136
|
+
const jiraFieldErrors = error.response?.data?.errors;
|
|
137
|
+
|
|
80
138
|
const errorResponse = {
|
|
81
139
|
error: 'Operation failed',
|
|
82
140
|
message: error.message || 'An unexpected error occurred',
|
|
83
141
|
};
|
|
84
142
|
|
|
143
|
+
if (jiraErrors?.length) {
|
|
144
|
+
errorResponse.jiraErrors = jiraErrors;
|
|
145
|
+
}
|
|
146
|
+
if (jiraFieldErrors && Object.keys(jiraFieldErrors).length > 0) {
|
|
147
|
+
errorResponse.fieldErrors = jiraFieldErrors;
|
|
148
|
+
}
|
|
149
|
+
|
|
85
150
|
if (isDevelopment) {
|
|
86
|
-
errorResponse.details = error.response?.data;
|
|
87
151
|
errorResponse.stack = error.stack;
|
|
88
152
|
}
|
|
89
153
|
|
|
@@ -121,131 +185,151 @@ const server = new Server(
|
|
|
121
185
|
}
|
|
122
186
|
);
|
|
123
187
|
|
|
188
|
+
function parseInlineContent(text) {
|
|
189
|
+
if (!text) return [];
|
|
190
|
+
|
|
191
|
+
const parts = [];
|
|
192
|
+
const regex = /\*\*([^*]+)\*\*|~~([^~]+)~~|\*([^*]+)\*|\[([^\]]+)\]\(([^)]+)\)|\[([^\]]+)\|([^\]]+)\]|`([^`]+)`/g;
|
|
193
|
+
let lastIndex = 0;
|
|
194
|
+
let match;
|
|
195
|
+
|
|
196
|
+
while ((match = regex.exec(text)) !== null) {
|
|
197
|
+
if (match.index > lastIndex) {
|
|
198
|
+
parts.push({ type: 'text', text: text.substring(lastIndex, match.index) });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (match[1] !== undefined) {
|
|
202
|
+
parts.push({ type: 'text', text: match[1], marks: [{ type: 'strong' }] });
|
|
203
|
+
} else if (match[2] !== undefined) {
|
|
204
|
+
parts.push({ type: 'text', text: match[2], marks: [{ type: 'strike' }] });
|
|
205
|
+
} else if (match[3] !== undefined) {
|
|
206
|
+
parts.push({ type: 'text', text: match[3], marks: [{ type: 'em' }] });
|
|
207
|
+
} else if (match[4] !== undefined) {
|
|
208
|
+
parts.push({ type: 'text', text: match[4], marks: [{ type: 'link', attrs: { href: match[5] } }] });
|
|
209
|
+
} else if (match[6] !== undefined) {
|
|
210
|
+
parts.push({ type: 'text', text: match[6], marks: [{ type: 'link', attrs: { href: match[7] } }] });
|
|
211
|
+
} else if (match[8] !== undefined) {
|
|
212
|
+
parts.push({ type: 'text', text: match[8], marks: [{ type: 'code' }] });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
lastIndex = regex.lastIndex;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (lastIndex < text.length) {
|
|
219
|
+
parts.push({ type: 'text', text: text.substring(lastIndex) });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (parts.length > 0) return parts;
|
|
223
|
+
return text ? [{ type: 'text', text }] : [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function addBulletItem(nodes, content) {
|
|
227
|
+
const listItem = {
|
|
228
|
+
type: 'listItem',
|
|
229
|
+
content: [{ type: 'paragraph', content }]
|
|
230
|
+
};
|
|
231
|
+
const lastNode = nodes[nodes.length - 1];
|
|
232
|
+
if (lastNode && lastNode.type === 'bulletList') {
|
|
233
|
+
lastNode.content.push(listItem);
|
|
234
|
+
} else {
|
|
235
|
+
nodes.push({ type: 'bulletList', content: [listItem] });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function addOrderedItem(nodes, content) {
|
|
240
|
+
const listItem = {
|
|
241
|
+
type: 'listItem',
|
|
242
|
+
content: [{ type: 'paragraph', content }]
|
|
243
|
+
};
|
|
244
|
+
const lastNode = nodes[nodes.length - 1];
|
|
245
|
+
if (lastNode && lastNode.type === 'orderedList') {
|
|
246
|
+
lastNode.content.push(listItem);
|
|
247
|
+
} else {
|
|
248
|
+
nodes.push({ type: 'orderedList', content: [listItem] });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
124
252
|
function createADFDocument(content) {
|
|
253
|
+
if (!content || typeof content !== 'string') {
|
|
254
|
+
return {
|
|
255
|
+
type: 'doc',
|
|
256
|
+
version: 1,
|
|
257
|
+
content: [{ type: 'paragraph', content: [] }]
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
125
261
|
const nodes = [];
|
|
126
262
|
const lines = content.split('\n');
|
|
127
263
|
|
|
128
264
|
for (let i = 0; i < lines.length; i++) {
|
|
129
265
|
const line = lines[i].trim();
|
|
130
266
|
|
|
131
|
-
if (!line)
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
267
|
+
if (!line) continue;
|
|
134
268
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
content: [{ type: 'text', text: line.substring(4) }]
|
|
140
|
-
});
|
|
141
|
-
} else if (line.startsWith('h2. ')) {
|
|
269
|
+
const jiraHeading = line.match(/^h([1-6])\.\s+(.+)/);
|
|
270
|
+
const mdHeading = line.match(/^(#{1,6})\s+(.+)/);
|
|
271
|
+
|
|
272
|
+
if (jiraHeading) {
|
|
142
273
|
nodes.push({
|
|
143
274
|
type: 'heading',
|
|
144
|
-
attrs: { level:
|
|
145
|
-
content: [
|
|
275
|
+
attrs: { level: parseInt(jiraHeading[1]) },
|
|
276
|
+
content: parseInlineContent(jiraHeading[2])
|
|
146
277
|
});
|
|
147
|
-
} else if (
|
|
278
|
+
} else if (mdHeading) {
|
|
148
279
|
nodes.push({
|
|
149
280
|
type: 'heading',
|
|
150
|
-
attrs: { level:
|
|
151
|
-
content: [
|
|
281
|
+
attrs: { level: mdHeading[1].length },
|
|
282
|
+
content: parseInlineContent(mdHeading[2])
|
|
152
283
|
});
|
|
153
|
-
} else if (line.startsWith('
|
|
154
|
-
|
|
155
|
-
|
|
284
|
+
} else if (line.startsWith('* ') || line.startsWith('- ')) {
|
|
285
|
+
addBulletItem(nodes, parseInlineContent(line.substring(2)));
|
|
286
|
+
} else if (/^\d+\.\s+/.test(line)) {
|
|
287
|
+
addOrderedItem(nodes, parseInlineContent(line.replace(/^\d+\.\s+/, '')));
|
|
288
|
+
} else if (line.startsWith('> ')) {
|
|
289
|
+
const text = line.substring(2);
|
|
290
|
+
const lastNode = nodes[nodes.length - 1];
|
|
291
|
+
if (lastNode && lastNode.type === 'blockquote') {
|
|
292
|
+
lastNode.content.push({
|
|
293
|
+
type: 'paragraph',
|
|
294
|
+
content: parseInlineContent(text)
|
|
295
|
+
});
|
|
296
|
+
} else {
|
|
156
297
|
nodes.push({
|
|
157
|
-
type: '
|
|
158
|
-
content: [{
|
|
159
|
-
type: 'listItem',
|
|
160
|
-
content: [{
|
|
161
|
-
type: 'paragraph',
|
|
162
|
-
content: [
|
|
163
|
-
{
|
|
164
|
-
type: 'text',
|
|
165
|
-
text: match[1],
|
|
166
|
-
marks: [{
|
|
167
|
-
type: 'link',
|
|
168
|
-
attrs: { href: match[2] }
|
|
169
|
-
}]
|
|
170
|
-
},
|
|
171
|
-
{ type: 'text', text: ' ' + match[3] }
|
|
172
|
-
]
|
|
173
|
-
}]
|
|
174
|
-
}]
|
|
298
|
+
type: 'blockquote',
|
|
299
|
+
content: [{ type: 'paragraph', content: parseInlineContent(text) }]
|
|
175
300
|
});
|
|
176
301
|
}
|
|
177
|
-
} else if (line
|
|
178
|
-
nodes.push({
|
|
179
|
-
type: 'bulletList',
|
|
180
|
-
content: [{
|
|
181
|
-
type: 'listItem',
|
|
182
|
-
content: [{
|
|
183
|
-
type: 'paragraph',
|
|
184
|
-
content: [{ type: 'text', text: line.substring(2) }]
|
|
185
|
-
}]
|
|
186
|
-
}]
|
|
187
|
-
});
|
|
188
|
-
} else if (line === '----') {
|
|
189
|
-
nodes.push({
|
|
190
|
-
type: 'rule'
|
|
191
|
-
});
|
|
302
|
+
} else if (line === '----' || line === '---') {
|
|
303
|
+
nodes.push({ type: 'rule' });
|
|
192
304
|
} else if (line === '```' || line.startsWith('```')) {
|
|
305
|
+
const lang = line.length > 3 ? line.substring(3).trim() : null;
|
|
193
306
|
const codeLines = [];
|
|
194
307
|
i++;
|
|
195
308
|
while (i < lines.length && lines[i].trim() !== '```') {
|
|
196
309
|
codeLines.push(lines[i]);
|
|
197
310
|
i++;
|
|
198
311
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
text: codeLines.join('\n')
|
|
204
|
-
}]
|
|
205
|
-
});
|
|
206
|
-
} else if (line.includes('*') && line.includes(':')) {
|
|
207
|
-
const parts = [];
|
|
208
|
-
const regex = /\*([^*]+)\*/g;
|
|
209
|
-
let lastIndex = 0;
|
|
210
|
-
let match;
|
|
211
|
-
|
|
212
|
-
while ((match = regex.exec(line)) !== null) {
|
|
213
|
-
if (match.index > lastIndex) {
|
|
214
|
-
parts.push({ type: 'text', text: line.substring(lastIndex, match.index) });
|
|
215
|
-
}
|
|
216
|
-
parts.push({
|
|
217
|
-
type: 'text',
|
|
218
|
-
text: match[1],
|
|
219
|
-
marks: [{ type: 'strong' }]
|
|
220
|
-
});
|
|
221
|
-
lastIndex = regex.lastIndex;
|
|
312
|
+
const codeText = codeLines.join('\n');
|
|
313
|
+
const codeBlock = { type: 'codeBlock' };
|
|
314
|
+
if (codeText) {
|
|
315
|
+
codeBlock.content = [{ type: 'text', text: codeText }];
|
|
222
316
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
parts.push({ type: 'text', text: line.substring(lastIndex) });
|
|
317
|
+
if (lang) {
|
|
318
|
+
codeBlock.attrs = { language: lang };
|
|
226
319
|
}
|
|
227
|
-
|
|
228
|
-
nodes.push({
|
|
229
|
-
type: 'paragraph',
|
|
230
|
-
content: parts
|
|
231
|
-
});
|
|
232
|
-
} else if (line.startsWith('*') && line.endsWith('*')) {
|
|
233
|
-
nodes.push({
|
|
234
|
-
type: 'paragraph',
|
|
235
|
-
content: [{
|
|
236
|
-
type: 'text',
|
|
237
|
-
text: line.substring(1, line.length - 1),
|
|
238
|
-
marks: [{ type: 'strong' }]
|
|
239
|
-
}]
|
|
240
|
-
});
|
|
320
|
+
nodes.push(codeBlock);
|
|
241
321
|
} else {
|
|
242
322
|
nodes.push({
|
|
243
323
|
type: 'paragraph',
|
|
244
|
-
content:
|
|
324
|
+
content: parseInlineContent(line)
|
|
245
325
|
});
|
|
246
326
|
}
|
|
247
327
|
}
|
|
248
328
|
|
|
329
|
+
if (nodes.length === 0) {
|
|
330
|
+
nodes.push({ type: 'paragraph', content: [] });
|
|
331
|
+
}
|
|
332
|
+
|
|
249
333
|
return {
|
|
250
334
|
type: 'doc',
|
|
251
335
|
version: 1,
|
|
@@ -253,6 +337,79 @@ function createADFDocument(content) {
|
|
|
253
337
|
};
|
|
254
338
|
}
|
|
255
339
|
|
|
340
|
+
function inlineNodesToText(nodes) {
|
|
341
|
+
if (!Array.isArray(nodes)) return '';
|
|
342
|
+
return nodes.map(node => {
|
|
343
|
+
if (node.type === 'text') {
|
|
344
|
+
let text = node.text || '';
|
|
345
|
+
if (node.marks) {
|
|
346
|
+
for (const mark of node.marks) {
|
|
347
|
+
switch (mark.type) {
|
|
348
|
+
case 'strong': text = `**${text}**`; break;
|
|
349
|
+
case 'em': text = `*${text}*`; break;
|
|
350
|
+
case 'strike': text = `~~${text}~~`; break;
|
|
351
|
+
case 'code': text = `\`${text}\``; break;
|
|
352
|
+
case 'link': text = `[${text}](${mark.attrs?.href || ''})`; break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return text;
|
|
357
|
+
}
|
|
358
|
+
if (node.type === 'hardBreak') return '\n';
|
|
359
|
+
if (node.type === 'mention') return `@${node.attrs?.text || node.attrs?.id || ''}`;
|
|
360
|
+
if (node.type === 'inlineCard') return node.attrs?.url || '';
|
|
361
|
+
if (node.type === 'emoji') return node.attrs?.shortName || '';
|
|
362
|
+
return '';
|
|
363
|
+
}).join('');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function blockNodeToText(node) {
|
|
367
|
+
if (!node) return '';
|
|
368
|
+
switch (node.type) {
|
|
369
|
+
case 'paragraph':
|
|
370
|
+
return inlineNodesToText(node.content);
|
|
371
|
+
case 'heading': {
|
|
372
|
+
const level = node.attrs?.level || 1;
|
|
373
|
+
return '#'.repeat(level) + ' ' + inlineNodesToText(node.content);
|
|
374
|
+
}
|
|
375
|
+
case 'bulletList':
|
|
376
|
+
return (node.content || []).map(item =>
|
|
377
|
+
'- ' + (item.content || []).map(c => blockNodeToText(c)).join('\n')
|
|
378
|
+
).join('\n');
|
|
379
|
+
case 'orderedList':
|
|
380
|
+
return (node.content || []).map((item, i) =>
|
|
381
|
+
`${i + 1}. ` + (item.content || []).map(c => blockNodeToText(c)).join('\n')
|
|
382
|
+
).join('\n');
|
|
383
|
+
case 'blockquote':
|
|
384
|
+
return (node.content || []).map(c => '> ' + blockNodeToText(c)).join('\n');
|
|
385
|
+
case 'codeBlock': {
|
|
386
|
+
const lang = node.attrs?.language || '';
|
|
387
|
+
const code = inlineNodesToText(node.content);
|
|
388
|
+
return '```' + lang + '\n' + code + '\n```';
|
|
389
|
+
}
|
|
390
|
+
case 'rule':
|
|
391
|
+
return '---';
|
|
392
|
+
case 'table':
|
|
393
|
+
return (node.content || []).map(row =>
|
|
394
|
+
'| ' + (row.content || []).map(cell =>
|
|
395
|
+
(cell.content || []).map(c => blockNodeToText(c)).join(' ')
|
|
396
|
+
).join(' | ') + ' |'
|
|
397
|
+
).join('\n');
|
|
398
|
+
case 'mediaSingle':
|
|
399
|
+
case 'mediaGroup':
|
|
400
|
+
return '[media]';
|
|
401
|
+
default:
|
|
402
|
+
return inlineNodesToText(node.content);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function adfToText(doc) {
|
|
407
|
+
if (!doc || doc.type !== 'doc' || !Array.isArray(doc.content)) {
|
|
408
|
+
return typeof doc === 'string' ? doc : '';
|
|
409
|
+
}
|
|
410
|
+
return doc.content.map(node => blockNodeToText(node)).join('\n\n');
|
|
411
|
+
}
|
|
412
|
+
|
|
256
413
|
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
257
414
|
return {
|
|
258
415
|
prompts: [
|
|
@@ -272,57 +429,24 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
272
429
|
role: 'user',
|
|
273
430
|
content: {
|
|
274
431
|
type: 'text',
|
|
275
|
-
text: `
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
1. Headers:
|
|
294
|
-
h1. Main Title
|
|
295
|
-
h2. Section Title
|
|
296
|
-
h3. Subsection
|
|
297
|
-
|
|
298
|
-
2. Lists:
|
|
299
|
-
* Bullet point
|
|
300
|
-
* Another bullet
|
|
301
|
-
|
|
302
|
-
# Numbered item
|
|
303
|
-
# Another number
|
|
304
|
-
|
|
305
|
-
3. Code Blocks:
|
|
306
|
-
\`\`\`
|
|
307
|
-
Error message or code here
|
|
308
|
-
Multiple lines supported
|
|
309
|
-
\`\`\`
|
|
310
|
-
|
|
311
|
-
4. Bold Text:
|
|
312
|
-
*important text*
|
|
313
|
-
|
|
314
|
-
5. Horizontal Line:
|
|
315
|
-
----
|
|
316
|
-
|
|
317
|
-
IMPORTANT RULES:
|
|
318
|
-
================
|
|
319
|
-
1. NEVER reference Jira issues as plain text like "PROJ-123"
|
|
320
|
-
2. ALWAYS use the format: - [KEY|URL] description
|
|
321
|
-
3. ALWAYS include the full URL: https://domain.atlassian.net/browse/KEY
|
|
322
|
-
4. The dash and space before the bracket are REQUIRED: "- ["
|
|
323
|
-
5. Use the pipe character | to separate key from URL
|
|
324
|
-
|
|
325
|
-
When creating or updating Jira issues, descriptions, or comments, automatically apply this formatting without being asked.`,
|
|
432
|
+
text: `This MCP server automatically converts Markdown to Atlassian Document Format (ADF).
|
|
433
|
+
|
|
434
|
+
Use standard Markdown:
|
|
435
|
+
|
|
436
|
+
Headings: # H1, ## H2, ### H3, #### H4, ##### H5, ###### H6
|
|
437
|
+
Bold: **bold text**
|
|
438
|
+
Italic: *italic text*
|
|
439
|
+
Strikethrough: ~~deleted text~~
|
|
440
|
+
Inline code: \`code\`
|
|
441
|
+
Links: [text](https://example.com)
|
|
442
|
+
Bullet lists: - item
|
|
443
|
+
Numbered lists: 1. item
|
|
444
|
+
Blockquotes: > text
|
|
445
|
+
Code blocks: \`\`\`language ... \`\`\`
|
|
446
|
+
Horizontal rule: ---
|
|
447
|
+
|
|
448
|
+
When referencing Jira issues, always use clickable links:
|
|
449
|
+
[PROJ-123](https://your-domain.atlassian.net/browse/PROJ-123)`,
|
|
326
450
|
},
|
|
327
451
|
},
|
|
328
452
|
],
|
|
@@ -337,22 +461,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
337
461
|
tools: [
|
|
338
462
|
{
|
|
339
463
|
name: 'jira_create_issue',
|
|
340
|
-
description:
|
|
341
|
-
|
|
342
|
-
⚠️ CRITICAL - ALWAYS Use Jira Formatting:
|
|
343
|
-
When writing descriptions, ALWAYS format Jira issue references as clickable links:
|
|
344
|
-
- [PROJECT-123|https://your-domain.atlassian.net/browse/PROJECT-123] Description
|
|
345
|
-
|
|
346
|
-
NEVER use plain text like "PROJECT-123" - it won't be clickable!
|
|
347
|
-
|
|
348
|
-
Supported formatting:
|
|
349
|
-
- h1. h2. h3. for headers
|
|
350
|
-
- * for bullet lists
|
|
351
|
-
- \`\`\` for code blocks
|
|
352
|
-
- *text* for bold
|
|
353
|
-
- ---- for horizontal rule
|
|
354
|
-
|
|
355
|
-
See the 'jira-formatting-guide' prompt for complete reference.`,
|
|
464
|
+
description: 'Create a new Jira issue. Description supports standard Markdown (headings, **bold**, [links](url), lists, code blocks). Automatically converted to ADF.',
|
|
356
465
|
inputSchema: {
|
|
357
466
|
type: 'object',
|
|
358
467
|
properties: {
|
|
@@ -362,7 +471,7 @@ See the 'jira-formatting-guide' prompt for complete reference.`,
|
|
|
362
471
|
},
|
|
363
472
|
description: {
|
|
364
473
|
type: 'string',
|
|
365
|
-
description: 'Issue description
|
|
474
|
+
description: 'Issue description in Markdown. Use [KEY](url) for clickable issue links.',
|
|
366
475
|
},
|
|
367
476
|
issueType: {
|
|
368
477
|
type: 'string',
|
|
@@ -381,7 +490,11 @@ See the 'jira-formatting-guide' prompt for complete reference.`,
|
|
|
381
490
|
},
|
|
382
491
|
storyPoints: {
|
|
383
492
|
type: 'number',
|
|
384
|
-
description: 'Story points estimate',
|
|
493
|
+
description: 'Story points estimate (0-1000)',
|
|
494
|
+
},
|
|
495
|
+
projectKey: {
|
|
496
|
+
type: 'string',
|
|
497
|
+
description: 'Project key (defaults to configured JIRA_PROJECT_KEY)',
|
|
385
498
|
},
|
|
386
499
|
},
|
|
387
500
|
required: ['summary', 'description'],
|
|
@@ -413,7 +526,7 @@ See the 'jira-formatting-guide' prompt for complete reference.`,
|
|
|
413
526
|
},
|
|
414
527
|
maxResults: {
|
|
415
528
|
type: 'number',
|
|
416
|
-
description: 'Maximum number of results',
|
|
529
|
+
description: 'Maximum number of results (1-100)',
|
|
417
530
|
default: 50,
|
|
418
531
|
},
|
|
419
532
|
},
|
|
@@ -422,45 +535,7 @@ See the 'jira-formatting-guide' prompt for complete reference.`,
|
|
|
422
535
|
},
|
|
423
536
|
{
|
|
424
537
|
name: 'jira_update_issue',
|
|
425
|
-
description:
|
|
426
|
-
|
|
427
|
-
IMPORTANT - Description Formatting Guide:
|
|
428
|
-
|
|
429
|
-
The description field supports a special markup format that gets converted to Atlassian Document Format (ADF):
|
|
430
|
-
|
|
431
|
-
1. HEADINGS:
|
|
432
|
-
h1. Heading 1
|
|
433
|
-
h2. Heading 2
|
|
434
|
-
h3. Heading 3
|
|
435
|
-
h4. Heading 4
|
|
436
|
-
h5. Heading 5
|
|
437
|
-
|
|
438
|
-
2. LISTS:
|
|
439
|
-
* Bullet item (use asterisk + space)
|
|
440
|
-
# Numbered item (use hash + space)
|
|
441
|
-
|
|
442
|
-
3. LINKS TO JIRA ISSUES (CREATES CLICKABLE LINKS):
|
|
443
|
-
- [ISSUE-KEY|URL] Description text
|
|
444
|
-
Example: - [PROJ-61|https://your-domain.atlassian.net/browse/PROJ-61] API rate limiting
|
|
445
|
-
|
|
446
|
-
This format is CRITICAL for creating active hyperlinks to Jira issues!
|
|
447
|
-
DO NOT use plain text like "PROJ-61" - it will not be clickable.
|
|
448
|
-
DO NOT use markdown format [text](url) - it will not work.
|
|
449
|
-
ALWAYS use the pipe format: [KEY|URL]
|
|
450
|
-
|
|
451
|
-
4. TEXT FORMATTING:
|
|
452
|
-
*bold text* (asterisk before and after)
|
|
453
|
-
|
|
454
|
-
5. HORIZONTAL RULE:
|
|
455
|
-
---- (four dashes)
|
|
456
|
-
|
|
457
|
-
Example with links:
|
|
458
|
-
h2. Task List
|
|
459
|
-
h4. Security Tasks
|
|
460
|
-
- [PROJ-61|https://your-domain.atlassian.net/browse/PROJ-61] Implement rate limiting
|
|
461
|
-
- [PROJ-63|https://your-domain.atlassian.net/browse/PROJ-63] Configure CORS
|
|
462
|
-
|
|
463
|
-
This will create proper clickable links in Jira UI.`,
|
|
538
|
+
description: 'Update a Jira issue. Description supports standard Markdown (headings, **bold**, [links](url), lists, code blocks). Automatically converted to ADF.',
|
|
464
539
|
inputSchema: {
|
|
465
540
|
type: 'object',
|
|
466
541
|
properties: {
|
|
@@ -474,7 +549,7 @@ This will create proper clickable links in Jira UI.`,
|
|
|
474
549
|
},
|
|
475
550
|
description: {
|
|
476
551
|
type: 'string',
|
|
477
|
-
description: 'New description
|
|
552
|
+
description: 'New description in Markdown. Use [KEY](url) for clickable issue links.',
|
|
478
553
|
},
|
|
479
554
|
status: {
|
|
480
555
|
type: 'string',
|
|
@@ -486,13 +561,7 @@ This will create proper clickable links in Jira UI.`,
|
|
|
486
561
|
},
|
|
487
562
|
{
|
|
488
563
|
name: 'jira_add_comment',
|
|
489
|
-
description:
|
|
490
|
-
|
|
491
|
-
IMPORTANT - Comment Formatting:
|
|
492
|
-
To create CLICKABLE LINKS to other Jira issues, use this format:
|
|
493
|
-
- [PROJ-123|https://your-domain.atlassian.net/browse/PROJ-123] Task description
|
|
494
|
-
|
|
495
|
-
See jira_update_issue tool description for complete formatting guide.`,
|
|
564
|
+
description: 'Add a comment to a Jira issue. Supports standard Markdown, automatically converted to ADF.',
|
|
496
565
|
inputSchema: {
|
|
497
566
|
type: 'object',
|
|
498
567
|
properties: {
|
|
@@ -502,7 +571,7 @@ See jira_update_issue tool description for complete formatting guide.`,
|
|
|
502
571
|
},
|
|
503
572
|
comment: {
|
|
504
573
|
type: 'string',
|
|
505
|
-
description: 'Comment text
|
|
574
|
+
description: 'Comment text in Markdown.',
|
|
506
575
|
},
|
|
507
576
|
},
|
|
508
577
|
required: ['issueKey', 'comment'],
|
|
@@ -561,13 +630,7 @@ See jira_update_issue tool description for complete formatting guide.`,
|
|
|
561
630
|
},
|
|
562
631
|
{
|
|
563
632
|
name: 'jira_create_subtask',
|
|
564
|
-
description:
|
|
565
|
-
|
|
566
|
-
IMPORTANT - Description Formatting:
|
|
567
|
-
To create CLICKABLE LINKS to other Jira issues, use this format:
|
|
568
|
-
- [PROJ-123|https://your-domain.atlassian.net/browse/PROJ-123] Task description
|
|
569
|
-
|
|
570
|
-
See jira_update_issue tool description for complete formatting guide.`,
|
|
633
|
+
description: 'Create a subtask under a parent issue. Description supports standard Markdown, automatically converted to ADF.',
|
|
571
634
|
inputSchema: {
|
|
572
635
|
type: 'object',
|
|
573
636
|
properties: {
|
|
@@ -581,17 +644,217 @@ See jira_update_issue tool description for complete formatting guide.`,
|
|
|
581
644
|
},
|
|
582
645
|
description: {
|
|
583
646
|
type: 'string',
|
|
584
|
-
description: 'Subtask description
|
|
647
|
+
description: 'Subtask description in Markdown. Use [KEY](url) for clickable issue links.',
|
|
585
648
|
},
|
|
586
649
|
priority: {
|
|
587
650
|
type: 'string',
|
|
588
651
|
description: 'Priority (Highest, High, Medium, Low, Lowest)',
|
|
589
652
|
default: 'Medium',
|
|
590
653
|
},
|
|
654
|
+
projectKey: {
|
|
655
|
+
type: 'string',
|
|
656
|
+
description: 'Project key (defaults to configured JIRA_PROJECT_KEY)',
|
|
657
|
+
},
|
|
591
658
|
},
|
|
592
659
|
required: ['parentKey', 'summary', 'description'],
|
|
593
660
|
},
|
|
594
661
|
},
|
|
662
|
+
{
|
|
663
|
+
name: 'jira_assign_issue',
|
|
664
|
+
description: 'Assign or unassign a user to a Jira issue. Pass null accountId to unassign.',
|
|
665
|
+
inputSchema: {
|
|
666
|
+
type: 'object',
|
|
667
|
+
properties: {
|
|
668
|
+
issueKey: {
|
|
669
|
+
type: 'string',
|
|
670
|
+
description: 'Issue key (e.g., TTC-123)',
|
|
671
|
+
},
|
|
672
|
+
accountId: {
|
|
673
|
+
type: ['string', 'null'],
|
|
674
|
+
description: 'Atlassian account ID of the assignee, or null to unassign',
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
required: ['issueKey'],
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: 'jira_list_transitions',
|
|
682
|
+
description: 'Get available status transitions for a Jira issue.',
|
|
683
|
+
inputSchema: {
|
|
684
|
+
type: 'object',
|
|
685
|
+
properties: {
|
|
686
|
+
issueKey: {
|
|
687
|
+
type: 'string',
|
|
688
|
+
description: 'Issue key (e.g., TTC-123)',
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
required: ['issueKey'],
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: 'jira_add_worklog',
|
|
696
|
+
description: 'Add a worklog entry (time tracking) to a Jira issue.',
|
|
697
|
+
inputSchema: {
|
|
698
|
+
type: 'object',
|
|
699
|
+
properties: {
|
|
700
|
+
issueKey: {
|
|
701
|
+
type: 'string',
|
|
702
|
+
description: 'Issue key (e.g., TTC-123)',
|
|
703
|
+
},
|
|
704
|
+
timeSpent: {
|
|
705
|
+
type: 'string',
|
|
706
|
+
description: 'Time spent in Jira format (e.g., "2h 30m", "1d", "45m")',
|
|
707
|
+
},
|
|
708
|
+
comment: {
|
|
709
|
+
type: 'string',
|
|
710
|
+
description: 'Worklog comment in Markdown.',
|
|
711
|
+
},
|
|
712
|
+
started: {
|
|
713
|
+
type: 'string',
|
|
714
|
+
description: 'Start date/time in ISO 8601 format (e.g., "2024-01-15T09:00:00.000+0000"). Defaults to now.',
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
required: ['issueKey', 'timeSpent'],
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
name: 'jira_get_comments',
|
|
722
|
+
description: 'Get comments from a Jira issue.',
|
|
723
|
+
inputSchema: {
|
|
724
|
+
type: 'object',
|
|
725
|
+
properties: {
|
|
726
|
+
issueKey: {
|
|
727
|
+
type: 'string',
|
|
728
|
+
description: 'Issue key (e.g., TTC-123)',
|
|
729
|
+
},
|
|
730
|
+
maxResults: {
|
|
731
|
+
type: 'number',
|
|
732
|
+
description: 'Maximum number of comments (1-100)',
|
|
733
|
+
default: 50,
|
|
734
|
+
},
|
|
735
|
+
orderBy: {
|
|
736
|
+
type: 'string',
|
|
737
|
+
description: 'Order by created date: "created" (oldest first) or "-created" (newest first)',
|
|
738
|
+
default: '-created',
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
required: ['issueKey'],
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: 'jira_get_worklogs',
|
|
746
|
+
description: 'Get worklog entries from a Jira issue.',
|
|
747
|
+
inputSchema: {
|
|
748
|
+
type: 'object',
|
|
749
|
+
properties: {
|
|
750
|
+
issueKey: {
|
|
751
|
+
type: 'string',
|
|
752
|
+
description: 'Issue key (e.g., TTC-123)',
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
required: ['issueKey'],
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
name: 'jira_list_projects',
|
|
760
|
+
description: 'List all accessible Jira projects.',
|
|
761
|
+
inputSchema: {
|
|
762
|
+
type: 'object',
|
|
763
|
+
properties: {
|
|
764
|
+
maxResults: {
|
|
765
|
+
type: 'number',
|
|
766
|
+
description: 'Maximum number of results (1-100)',
|
|
767
|
+
default: 50,
|
|
768
|
+
},
|
|
769
|
+
query: {
|
|
770
|
+
type: 'string',
|
|
771
|
+
description: 'Filter projects by name (partial match)',
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: 'jira_get_project_components',
|
|
778
|
+
description: 'Get components of a Jira project.',
|
|
779
|
+
inputSchema: {
|
|
780
|
+
type: 'object',
|
|
781
|
+
properties: {
|
|
782
|
+
projectKey: {
|
|
783
|
+
type: 'string',
|
|
784
|
+
description: 'Project key (defaults to configured JIRA_PROJECT_KEY)',
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: 'jira_get_project_versions',
|
|
791
|
+
description: 'Get versions (releases) of a Jira project.',
|
|
792
|
+
inputSchema: {
|
|
793
|
+
type: 'object',
|
|
794
|
+
properties: {
|
|
795
|
+
projectKey: {
|
|
796
|
+
type: 'string',
|
|
797
|
+
description: 'Project key (defaults to configured JIRA_PROJECT_KEY)',
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
name: 'jira_get_fields',
|
|
804
|
+
description: 'Get all available Jira fields. Useful for finding custom field IDs.',
|
|
805
|
+
inputSchema: {
|
|
806
|
+
type: 'object',
|
|
807
|
+
properties: {},
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: 'jira_get_issue_types',
|
|
812
|
+
description: 'Get all available issue types for a project.',
|
|
813
|
+
inputSchema: {
|
|
814
|
+
type: 'object',
|
|
815
|
+
properties: {
|
|
816
|
+
projectKey: {
|
|
817
|
+
type: 'string',
|
|
818
|
+
description: 'Project key (defaults to configured JIRA_PROJECT_KEY)',
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: 'jira_get_priorities',
|
|
825
|
+
description: 'Get all available issue priorities.',
|
|
826
|
+
inputSchema: {
|
|
827
|
+
type: 'object',
|
|
828
|
+
properties: {},
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
name: 'jira_get_link_types',
|
|
833
|
+
description: 'Get all available issue link types.',
|
|
834
|
+
inputSchema: {
|
|
835
|
+
type: 'object',
|
|
836
|
+
properties: {},
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
name: 'jira_search_users',
|
|
841
|
+
description: 'Search for Jira users by name or email. Returns accountId needed for jira_assign_issue.',
|
|
842
|
+
inputSchema: {
|
|
843
|
+
type: 'object',
|
|
844
|
+
properties: {
|
|
845
|
+
query: {
|
|
846
|
+
type: 'string',
|
|
847
|
+
description: 'Search query (matches display name and email prefix)',
|
|
848
|
+
},
|
|
849
|
+
maxResults: {
|
|
850
|
+
type: 'number',
|
|
851
|
+
description: 'Maximum number of results (1-100)',
|
|
852
|
+
default: 10,
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
required: ['query'],
|
|
856
|
+
},
|
|
857
|
+
},
|
|
595
858
|
],
|
|
596
859
|
};
|
|
597
860
|
});
|
|
@@ -603,20 +866,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
603
866
|
switch (name) {
|
|
604
867
|
case 'jira_create_issue': {
|
|
605
868
|
const { summary, description, issueType = 'Task', priority = 'Medium', labels = [], storyPoints } = args;
|
|
869
|
+
const projectKey = args.projectKey ? validateProjectKey(args.projectKey) : JIRA_PROJECT_KEY;
|
|
870
|
+
|
|
871
|
+
validateSafeParam(issueType, 'issueType');
|
|
872
|
+
validateSafeParam(priority, 'priority');
|
|
873
|
+
const validatedLabels = validateLabels(labels);
|
|
606
874
|
|
|
607
875
|
const issueData = {
|
|
608
876
|
fields: {
|
|
609
|
-
project: { key:
|
|
877
|
+
project: { key: projectKey },
|
|
610
878
|
summary: sanitizeString(summary, 500, 'summary'),
|
|
611
879
|
description: createADFDocument(description),
|
|
612
880
|
issuetype: { name: issueType },
|
|
613
881
|
priority: { name: priority },
|
|
614
|
-
labels,
|
|
882
|
+
labels: validatedLabels,
|
|
615
883
|
},
|
|
616
884
|
};
|
|
617
885
|
|
|
618
|
-
if (storyPoints) {
|
|
619
|
-
issueData.fields[STORY_POINTS_FIELD] = storyPoints;
|
|
886
|
+
if (storyPoints !== undefined && storyPoints !== null) {
|
|
887
|
+
issueData.fields[STORY_POINTS_FIELD] = validateStoryPoints(storyPoints);
|
|
620
888
|
}
|
|
621
889
|
|
|
622
890
|
const response = await jiraApi.post('/issue', issueData);
|
|
@@ -633,19 +901,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
633
901
|
const { issueKey } = args;
|
|
634
902
|
validateIssueKey(issueKey);
|
|
635
903
|
const response = await jiraApi.get(`/issue/${issueKey}`);
|
|
904
|
+
const f = response.data.fields;
|
|
636
905
|
|
|
637
906
|
return createSuccessResponse({
|
|
638
907
|
key: response.data.key,
|
|
639
|
-
summary:
|
|
640
|
-
description:
|
|
641
|
-
status:
|
|
642
|
-
assignee:
|
|
643
|
-
reporter:
|
|
644
|
-
priority:
|
|
645
|
-
issueType:
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
908
|
+
summary: f.summary,
|
|
909
|
+
description: adfToText(f.description),
|
|
910
|
+
status: f.status?.name,
|
|
911
|
+
assignee: f.assignee ? { displayName: f.assignee.displayName, accountId: f.assignee.accountId } : null,
|
|
912
|
+
reporter: f.reporter?.displayName,
|
|
913
|
+
priority: f.priority?.name,
|
|
914
|
+
issueType: f.issuetype?.name,
|
|
915
|
+
labels: f.labels || [],
|
|
916
|
+
storyPoints: f[STORY_POINTS_FIELD],
|
|
917
|
+
parent: f.parent?.key,
|
|
918
|
+
created: f.created,
|
|
919
|
+
updated: f.updated,
|
|
649
920
|
url: createIssueUrl(response.data.key),
|
|
650
921
|
});
|
|
651
922
|
}
|
|
@@ -653,12 +924,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
653
924
|
case 'jira_search_issues': {
|
|
654
925
|
const { jql, maxResults = 50 } = args;
|
|
655
926
|
validateJQL(jql);
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
927
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
928
|
+
|
|
929
|
+
const response = await jiraApi.post('/search', {
|
|
930
|
+
jql,
|
|
931
|
+
maxResults: validatedMaxResults,
|
|
932
|
+
fields: ['summary', 'status', 'assignee', 'priority', 'created', 'updated', 'issuetype', 'parent', 'labels'],
|
|
662
933
|
});
|
|
663
934
|
|
|
664
935
|
return createSuccessResponse({
|
|
@@ -666,10 +937,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
666
937
|
issues: response.data.issues.map(issue => ({
|
|
667
938
|
key: issue.key,
|
|
668
939
|
summary: issue.fields.summary,
|
|
669
|
-
status: issue.fields.status
|
|
670
|
-
assignee: issue.fields.assignee
|
|
940
|
+
status: issue.fields.status?.name,
|
|
941
|
+
assignee: issue.fields.assignee ? { displayName: issue.fields.assignee.displayName, accountId: issue.fields.assignee.accountId } : null,
|
|
671
942
|
priority: issue.fields.priority?.name,
|
|
672
943
|
issueType: issue.fields.issuetype?.name,
|
|
944
|
+
labels: issue.fields.labels || [],
|
|
673
945
|
parent: issue.fields.parent?.key,
|
|
674
946
|
url: createIssueUrl(issue.key),
|
|
675
947
|
})),
|
|
@@ -681,15 +953,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
681
953
|
validateIssueKey(issueKey);
|
|
682
954
|
|
|
683
955
|
const updateData = { fields: {} };
|
|
956
|
+
let hasFieldUpdates = false;
|
|
684
957
|
|
|
685
958
|
if (summary) {
|
|
686
959
|
updateData.fields.summary = sanitizeString(summary, 500, 'summary');
|
|
960
|
+
hasFieldUpdates = true;
|
|
687
961
|
}
|
|
688
962
|
if (description) {
|
|
689
963
|
updateData.fields.description = createADFDocument(description);
|
|
964
|
+
hasFieldUpdates = true;
|
|
690
965
|
}
|
|
691
966
|
|
|
692
|
-
|
|
967
|
+
if (hasFieldUpdates) {
|
|
968
|
+
await jiraApi.put(`/issue/${issueKey}`, updateData);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const warnings = [];
|
|
693
972
|
|
|
694
973
|
if (status) {
|
|
695
974
|
const transitions = await jiraApi.get(`/issue/${issueKey}/transitions`);
|
|
@@ -699,14 +978,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
699
978
|
await jiraApi.post(`/issue/${issueKey}/transitions`, {
|
|
700
979
|
transition: { id: transition.id },
|
|
701
980
|
});
|
|
981
|
+
} else {
|
|
982
|
+
const available = transitions.data.transitions.map(t => t.name).join(', ');
|
|
983
|
+
warnings.push(`Transition "${status}" not found. Available transitions: ${available}`);
|
|
702
984
|
}
|
|
703
985
|
}
|
|
704
986
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
987
|
+
if (!hasFieldUpdates && !status) {
|
|
988
|
+
return createSuccessResponse({
|
|
989
|
+
success: false,
|
|
990
|
+
message: `No updates provided for ${issueKey}`,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const result = {
|
|
995
|
+
success: warnings.length === 0,
|
|
996
|
+
message: `Issue ${issueKey} updated${warnings.length > 0 ? ' with warnings' : ' successfully'}`,
|
|
708
997
|
url: createIssueUrl(issueKey),
|
|
709
|
-
}
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
if (warnings.length > 0) {
|
|
1001
|
+
result.warnings = warnings;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return createSuccessResponse(result);
|
|
710
1005
|
}
|
|
711
1006
|
|
|
712
1007
|
case 'jira_add_comment': {
|
|
@@ -727,6 +1022,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
727
1022
|
const { inwardIssue, outwardIssue, linkType = 'Relates' } = args;
|
|
728
1023
|
validateIssueKey(inwardIssue);
|
|
729
1024
|
validateIssueKey(outwardIssue);
|
|
1025
|
+
validateSafeParam(linkType, 'linkType');
|
|
730
1026
|
|
|
731
1027
|
try {
|
|
732
1028
|
await jiraApi.post('/issueLink', {
|
|
@@ -754,6 +1050,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
754
1050
|
|
|
755
1051
|
case 'jira_get_project_info': {
|
|
756
1052
|
const { projectKey = JIRA_PROJECT_KEY } = args;
|
|
1053
|
+
validateProjectKey(projectKey);
|
|
757
1054
|
const response = await jiraApi.get(`/project/${projectKey}`);
|
|
758
1055
|
|
|
759
1056
|
return createSuccessResponse({
|
|
@@ -779,10 +1076,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
779
1076
|
case 'jira_create_subtask': {
|
|
780
1077
|
const { parentKey, summary, description, priority = 'Medium' } = args;
|
|
781
1078
|
validateIssueKey(parentKey);
|
|
1079
|
+
validateSafeParam(priority, 'priority');
|
|
1080
|
+
const projectKey = args.projectKey ? validateProjectKey(args.projectKey) : JIRA_PROJECT_KEY;
|
|
782
1081
|
|
|
783
1082
|
const issueData = {
|
|
784
1083
|
fields: {
|
|
785
|
-
project: { key:
|
|
1084
|
+
project: { key: projectKey },
|
|
786
1085
|
summary: sanitizeString(summary, 500, 'summary'),
|
|
787
1086
|
description: createADFDocument(description),
|
|
788
1087
|
issuetype: { name: 'Subtask' },
|
|
@@ -802,6 +1101,243 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
802
1101
|
});
|
|
803
1102
|
}
|
|
804
1103
|
|
|
1104
|
+
case 'jira_assign_issue': {
|
|
1105
|
+
const { issueKey, accountId } = args;
|
|
1106
|
+
validateIssueKey(issueKey);
|
|
1107
|
+
|
|
1108
|
+
await jiraApi.put(`/issue/${issueKey}/assignee`, {
|
|
1109
|
+
accountId: accountId !== undefined ? accountId : null,
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
return createSuccessResponse({
|
|
1113
|
+
success: true,
|
|
1114
|
+
message: accountId
|
|
1115
|
+
? `Issue ${issueKey} assigned to ${accountId}`
|
|
1116
|
+
: `Issue ${issueKey} unassigned`,
|
|
1117
|
+
url: createIssueUrl(issueKey),
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
case 'jira_list_transitions': {
|
|
1122
|
+
const { issueKey } = args;
|
|
1123
|
+
validateIssueKey(issueKey);
|
|
1124
|
+
|
|
1125
|
+
const response = await jiraApi.get(`/issue/${issueKey}/transitions`);
|
|
1126
|
+
|
|
1127
|
+
return createSuccessResponse({
|
|
1128
|
+
issueKey,
|
|
1129
|
+
transitions: response.data.transitions.map(t => ({
|
|
1130
|
+
id: t.id,
|
|
1131
|
+
name: t.name,
|
|
1132
|
+
to: {
|
|
1133
|
+
id: t.to.id,
|
|
1134
|
+
name: t.to.name,
|
|
1135
|
+
category: t.to.statusCategory?.name,
|
|
1136
|
+
},
|
|
1137
|
+
})),
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
case 'jira_add_worklog': {
|
|
1142
|
+
const { issueKey, timeSpent, comment, started } = args;
|
|
1143
|
+
validateIssueKey(issueKey);
|
|
1144
|
+
sanitizeString(timeSpent, 50, 'timeSpent');
|
|
1145
|
+
|
|
1146
|
+
const worklogData = { timeSpent };
|
|
1147
|
+
if (comment) {
|
|
1148
|
+
worklogData.comment = createADFDocument(comment);
|
|
1149
|
+
}
|
|
1150
|
+
if (started) {
|
|
1151
|
+
worklogData.started = started;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const response = await jiraApi.post(`/issue/${issueKey}/worklog`, worklogData);
|
|
1155
|
+
|
|
1156
|
+
return createSuccessResponse({
|
|
1157
|
+
success: true,
|
|
1158
|
+
id: response.data.id,
|
|
1159
|
+
issueKey,
|
|
1160
|
+
timeSpent: response.data.timeSpent,
|
|
1161
|
+
author: response.data.author?.displayName,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
case 'jira_get_comments': {
|
|
1166
|
+
const { issueKey, maxResults = 50, orderBy = '-created' } = args;
|
|
1167
|
+
validateIssueKey(issueKey);
|
|
1168
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1169
|
+
|
|
1170
|
+
const response = await jiraApi.get(`/issue/${issueKey}/comment`, {
|
|
1171
|
+
params: { maxResults: validatedMaxResults, orderBy },
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
return createSuccessResponse({
|
|
1175
|
+
issueKey,
|
|
1176
|
+
total: response.data.total,
|
|
1177
|
+
comments: response.data.comments.map(c => ({
|
|
1178
|
+
id: c.id,
|
|
1179
|
+
author: c.author?.displayName,
|
|
1180
|
+
body: adfToText(c.body),
|
|
1181
|
+
created: c.created,
|
|
1182
|
+
updated: c.updated,
|
|
1183
|
+
})),
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
case 'jira_get_worklogs': {
|
|
1188
|
+
const { issueKey } = args;
|
|
1189
|
+
validateIssueKey(issueKey);
|
|
1190
|
+
|
|
1191
|
+
const response = await jiraApi.get(`/issue/${issueKey}/worklog`);
|
|
1192
|
+
|
|
1193
|
+
return createSuccessResponse({
|
|
1194
|
+
issueKey,
|
|
1195
|
+
total: response.data.total,
|
|
1196
|
+
worklogs: response.data.worklogs.map(w => ({
|
|
1197
|
+
id: w.id,
|
|
1198
|
+
author: w.author?.displayName,
|
|
1199
|
+
timeSpent: w.timeSpent,
|
|
1200
|
+
timeSpentSeconds: w.timeSpentSeconds,
|
|
1201
|
+
started: w.started,
|
|
1202
|
+
comment: adfToText(w.comment),
|
|
1203
|
+
})),
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
case 'jira_list_projects': {
|
|
1208
|
+
const { maxResults = 50, query } = args;
|
|
1209
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1210
|
+
|
|
1211
|
+
const params = { maxResults: validatedMaxResults };
|
|
1212
|
+
if (query) {
|
|
1213
|
+
params.query = sanitizeString(query, 200, 'query');
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const response = await jiraApi.get('/project/search', { params });
|
|
1217
|
+
|
|
1218
|
+
return createSuccessResponse({
|
|
1219
|
+
total: response.data.total,
|
|
1220
|
+
projects: response.data.values.map(p => ({
|
|
1221
|
+
key: p.key,
|
|
1222
|
+
name: p.name,
|
|
1223
|
+
projectTypeKey: p.projectTypeKey,
|
|
1224
|
+
style: p.style,
|
|
1225
|
+
lead: p.lead?.displayName,
|
|
1226
|
+
})),
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
case 'jira_get_project_components': {
|
|
1231
|
+
const projectKey = args.projectKey ? validateProjectKey(args.projectKey) : JIRA_PROJECT_KEY;
|
|
1232
|
+
|
|
1233
|
+
const response = await jiraApi.get(`/project/${projectKey}/components`);
|
|
1234
|
+
|
|
1235
|
+
return createSuccessResponse({
|
|
1236
|
+
projectKey,
|
|
1237
|
+
components: response.data.map(c => ({
|
|
1238
|
+
id: c.id,
|
|
1239
|
+
name: c.name,
|
|
1240
|
+
description: c.description,
|
|
1241
|
+
lead: c.lead?.displayName,
|
|
1242
|
+
assigneeType: c.assigneeType,
|
|
1243
|
+
})),
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
case 'jira_get_project_versions': {
|
|
1248
|
+
const projectKey = args.projectKey ? validateProjectKey(args.projectKey) : JIRA_PROJECT_KEY;
|
|
1249
|
+
|
|
1250
|
+
const response = await jiraApi.get(`/project/${projectKey}/versions`);
|
|
1251
|
+
|
|
1252
|
+
return createSuccessResponse({
|
|
1253
|
+
projectKey,
|
|
1254
|
+
versions: response.data.map(v => ({
|
|
1255
|
+
id: v.id,
|
|
1256
|
+
name: v.name,
|
|
1257
|
+
description: v.description,
|
|
1258
|
+
released: v.released,
|
|
1259
|
+
archived: v.archived,
|
|
1260
|
+
releaseDate: v.releaseDate,
|
|
1261
|
+
startDate: v.startDate,
|
|
1262
|
+
})),
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
case 'jira_get_fields': {
|
|
1267
|
+
const response = await jiraApi.get('/field');
|
|
1268
|
+
|
|
1269
|
+
return createSuccessResponse({
|
|
1270
|
+
fields: response.data.map(f => ({
|
|
1271
|
+
id: f.id,
|
|
1272
|
+
name: f.name,
|
|
1273
|
+
custom: f.custom,
|
|
1274
|
+
schema: f.schema,
|
|
1275
|
+
})),
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
case 'jira_get_issue_types': {
|
|
1280
|
+
const projectKey = args.projectKey ? validateProjectKey(args.projectKey) : JIRA_PROJECT_KEY;
|
|
1281
|
+
|
|
1282
|
+
const response = await jiraApi.get(`/issue/createmeta/${projectKey}/issuetypes`);
|
|
1283
|
+
|
|
1284
|
+
return createSuccessResponse({
|
|
1285
|
+
projectKey,
|
|
1286
|
+
issueTypes: response.data.issueTypes.map(t => ({
|
|
1287
|
+
id: t.id,
|
|
1288
|
+
name: t.name,
|
|
1289
|
+
subtask: t.subtask,
|
|
1290
|
+
description: t.description,
|
|
1291
|
+
})),
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
case 'jira_get_priorities': {
|
|
1296
|
+
const response = await jiraApi.get('/priority');
|
|
1297
|
+
|
|
1298
|
+
return createSuccessResponse({
|
|
1299
|
+
priorities: response.data.map(p => ({
|
|
1300
|
+
id: p.id,
|
|
1301
|
+
name: p.name,
|
|
1302
|
+
description: p.description,
|
|
1303
|
+
iconUrl: p.iconUrl,
|
|
1304
|
+
})),
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
case 'jira_get_link_types': {
|
|
1309
|
+
const response = await jiraApi.get('/issueLinkType');
|
|
1310
|
+
|
|
1311
|
+
return createSuccessResponse({
|
|
1312
|
+
linkTypes: response.data.issueLinkTypes.map(lt => ({
|
|
1313
|
+
id: lt.id,
|
|
1314
|
+
name: lt.name,
|
|
1315
|
+
inward: lt.inward,
|
|
1316
|
+
outward: lt.outward,
|
|
1317
|
+
})),
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
case 'jira_search_users': {
|
|
1322
|
+
const { query, maxResults = 10 } = args;
|
|
1323
|
+
sanitizeString(query, 200, 'query');
|
|
1324
|
+
const validatedMaxResults = validateMaxResults(maxResults);
|
|
1325
|
+
|
|
1326
|
+
const response = await jiraApi.get('/user/search', {
|
|
1327
|
+
params: { query, maxResults: validatedMaxResults },
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
return createSuccessResponse({
|
|
1331
|
+
users: response.data.map(u => ({
|
|
1332
|
+
accountId: u.accountId,
|
|
1333
|
+
displayName: u.displayName,
|
|
1334
|
+
emailAddress: u.emailAddress,
|
|
1335
|
+
active: u.active,
|
|
1336
|
+
accountType: u.accountType,
|
|
1337
|
+
})),
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
805
1341
|
default:
|
|
806
1342
|
throw new Error(`Unknown tool: ${name}`);
|
|
807
1343
|
}
|
package/package.json
CHANGED