@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.
@@ -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] || fallback;
18
- if (!value) {
19
- throw new Error(`Required environment variable ${name} is not set. Please check your .env file.`);
17
+ const value = process.env[name];
18
+ if (value !== undefined && value !== '') {
19
+ return value;
20
20
  }
21
- return value;
22
- }
23
-
24
- const JIRA_URL = getRequiredEnv('JIRA_HOST', process.env.JIRA_URL);
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
- if (line.startsWith('h1. ')) {
136
- nodes.push({
137
- type: 'heading',
138
- attrs: { level: 1 },
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: 2 },
145
- content: [{ type: 'text', text: line.substring(4) }]
275
+ attrs: { level: parseInt(jiraHeading[1]) },
276
+ content: parseInlineContent(jiraHeading[2])
146
277
  });
147
- } else if (line.startsWith('h3. ')) {
278
+ } else if (mdHeading) {
148
279
  nodes.push({
149
280
  type: 'heading',
150
- attrs: { level: 3 },
151
- content: [{ type: 'text', text: line.substring(4) }]
281
+ attrs: { level: mdHeading[1].length },
282
+ content: parseInlineContent(mdHeading[2])
152
283
  });
153
- } else if (line.startsWith('- [') && line.includes('|')) {
154
- const match = line.match(/- \[([^\]]+)\|([^\]]+)\] (.+)/);
155
- if (match) {
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: 'bulletList',
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.startsWith('* ')) {
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
- nodes.push({
200
- type: 'codeBlock',
201
- content: [{
202
- type: 'text',
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
- if (lastIndex < line.length) {
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: [{ type: 'text', text: line }]
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: `When working with Jira through this MCP server, you MUST ALWAYS follow these formatting rules:
276
-
277
- CRITICAL: CLICKABLE ISSUE LINKS
278
- ================================
279
- To create clickable links to Jira issues, ALWAYS use this exact format:
280
- - [ISSUE-KEY|FULL-URL] Description text
281
-
282
- CORRECT Examples:
283
- - [PROJ-123|https://your-domain.atlassian.net/browse/PROJ-123] Implement authentication
284
- - [PROJ-124|https://your-domain.atlassian.net/browse/PROJ-124] Add unit tests
285
-
286
- WRONG (these will NOT be clickable):
287
- - PROJ-123 Implement authentication (plain text)
288
- - * PROJ-123 Implement authentication (plain bullet)
289
- - [PROJ-123](https://...) (markdown format)
290
-
291
- JIRA FORMATTING REFERENCE:
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: `Create a new Jira issue with proper ADF formatting.
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 - use format: - [KEY|URL] text for clickable links',
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: `Update a Jira issue.
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 - see tool description for formatting guide with clickable links',
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: `Add a comment to a Jira issue.
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 - use format: - [KEY|URL] text for clickable links',
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: `Create a subtask under a parent issue.
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 - use format: - [KEY|URL] text for clickable links',
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: JIRA_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: response.data.fields.summary,
640
- description: response.data.fields.description,
641
- status: response.data.fields.status.name,
642
- assignee: response.data.fields.assignee?.displayName,
643
- reporter: response.data.fields.reporter?.displayName,
644
- priority: response.data.fields.priority?.name,
645
- issueType: response.data.fields.issuetype?.name,
646
- parent: response.data.fields.parent?.key,
647
- created: response.data.fields.created,
648
- updated: response.data.fields.updated,
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 response = await jiraApi.get('/search/jql', {
657
- params: {
658
- jql,
659
- maxResults,
660
- fields: 'summary,status,assignee,priority,created,updated,issuetype,parent',
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.name,
670
- assignee: issue.fields.assignee?.displayName,
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
- await jiraApi.put(`/issue/${issueKey}`, updateData);
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
- return createSuccessResponse({
706
- success: true,
707
- message: `Issue ${issueKey} updated successfully`,
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: JIRA_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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpio/jira",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Model Context Protocol (MCP) server for Jira API integration with enhanced ADF formatting support and security hardening",
5
5
  "author": {
6
6
  "name": "Volodymyr Press",