@aaronsb/jira-cloud-mcp 0.4.0 → 0.4.2

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.
@@ -69,11 +69,17 @@ export class JiraClient {
69
69
  return [
70
70
  'summary',
71
71
  'description',
72
+ 'issuetype',
73
+ 'priority',
72
74
  'parent',
73
75
  'assignee',
74
76
  'reporter',
75
77
  'status',
76
78
  'resolution',
79
+ 'labels',
80
+ 'created',
81
+ 'updated',
82
+ 'resolutiondate',
77
83
  'duedate',
78
84
  this.customFields.startDate,
79
85
  this.customFields.storyPoints,
@@ -88,11 +94,17 @@ export class JiraClient {
88
94
  key: issue.key,
89
95
  summary: fields?.summary,
90
96
  description: issue.renderedFields?.description || '',
97
+ issueType: fields?.issuetype?.name || '',
98
+ priority: fields?.priority?.name || null,
91
99
  parent: fields?.parent?.key || null,
92
100
  assignee: fields?.assignee?.displayName || null,
93
101
  reporter: fields?.reporter?.displayName || '',
94
102
  status: fields?.status?.name || '',
95
103
  resolution: fields?.resolution?.name || null,
104
+ labels: fields?.labels || [],
105
+ created: fields?.created || '',
106
+ updated: fields?.updated || '',
107
+ resolutionDate: fields?.resolutiondate || null,
96
108
  dueDate: fields?.duedate || null,
97
109
  startDate: fields?.[this.customFields.startDate] || null,
98
110
  storyPoints: fields?.[this.customFields.storyPoints] || null,
@@ -378,6 +390,8 @@ export class JiraClient {
378
390
  fields.priority = { id: params.priority };
379
391
  if (params.labels)
380
392
  fields.labels = params.labels;
393
+ if (params.dueDate !== undefined)
394
+ fields.duedate = params.dueDate;
381
395
  if (params.customFields) {
382
396
  Object.assign(fields, this.convertAdfFields(params.customFields));
383
397
  }
@@ -877,6 +891,8 @@ export class JiraClient {
877
891
  fields.assignee = { accountId: params.assignee };
878
892
  if (params.labels)
879
893
  fields.labels = params.labels;
894
+ if (params.dueDate)
895
+ fields.duedate = params.dueDate;
880
896
  if (params.customFields) {
881
897
  Object.assign(fields, this.convertAdfFields(params.customFields));
882
898
  }
@@ -401,7 +401,7 @@ function generateQueueToolDocumentation(_schema) {
401
401
  description: "Execute multiple Jira operations in a single call. Operations run sequentially with result references and per-operation error strategies.",
402
402
  parameters: {
403
403
  operations: {
404
- type: "array (max 10)",
404
+ type: "array (max 16)",
405
405
  description: "Ordered list of operations. Each has: tool (string), args (object), onError ('bail' | 'continue', default 'bail').",
406
406
  },
407
407
  },
@@ -61,8 +61,9 @@ function validateManageJiraIssueArgs(args) {
61
61
  normalizedArgs.assignee === undefined &&
62
62
  normalizedArgs.priority === undefined &&
63
63
  normalizedArgs.labels === undefined &&
64
+ normalizedArgs.dueDate === undefined &&
64
65
  normalizedArgs.customFields === undefined) {
65
- throw new McpError(ErrorCode.InvalidParams, 'At least one update field (summary, description, parent, assignee, priority, labels, or customFields) must be provided for the update operation.');
66
+ throw new McpError(ErrorCode.InvalidParams, 'At least one update field (summary, description, parent, assignee, priority, labels, dueDate, or customFields) must be provided for the update operation.');
66
67
  }
67
68
  break;
68
69
  case 'transition':
@@ -255,6 +256,7 @@ async function handleCreateIssue(jiraClient, args) {
255
256
  priority: args.priority,
256
257
  assignee: args.assignee,
257
258
  labels: args.labels,
259
+ dueDate: args.dueDate ?? undefined,
258
260
  customFields,
259
261
  });
260
262
  // Get the created issue and render to markdown
@@ -279,6 +281,7 @@ async function handleUpdateIssue(jiraClient, args) {
279
281
  assignee: args.assignee,
280
282
  priority: args.priority,
281
283
  labels: args.labels,
284
+ dueDate: args.dueDate,
282
285
  customFields,
283
286
  });
284
287
  // Get the updated issue and render to markdown
@@ -10,7 +10,7 @@
10
10
  import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
11
11
  import { bulkOperationGuard } from '../utils/bulk-operation-guard.js';
12
12
  // ── Constants ──────────────────────────────────────────────────────────
13
- const MAX_OPERATIONS = 10;
13
+ const MAX_OPERATIONS = 16;
14
14
  const DESTRUCTIVE_OPERATIONS = new Set(['delete', 'move']);
15
15
  // ── Queue Handler ──────────────────────────────────────────────────────
16
16
  export function createQueueHandler(handlers, jiraHost) {
@@ -20,6 +20,7 @@ export function createQueueHandler(handlers, jiraHost) {
20
20
  throw new McpError(ErrorCode.InvalidParams, 'Missing required parameter: operations (array)');
21
21
  }
22
22
  const operations = args.operations;
23
+ const detail = args.detail === 'full' ? 'full' : 'summary';
23
24
  if (operations.length === 0) {
24
25
  throw new McpError(ErrorCode.InvalidParams, 'Operations list is empty.');
25
26
  }
@@ -100,7 +101,7 @@ export function createQueueHandler(handlers, jiraHost) {
100
101
  }
101
102
  }
102
103
  return {
103
- content: [{ type: 'text', text: formatResults(results, operations.length, bailedAt) }],
104
+ content: [{ type: 'text', text: formatResults(results, operations.length, bailedAt, detail) }],
104
105
  };
105
106
  };
106
107
  }
@@ -222,8 +223,8 @@ function firstLine(text) {
222
223
  }
223
224
  return stripped.slice(0, 100);
224
225
  }
225
- /** Format the queue results into a compact response */
226
- function formatResults(results, total, bailedAt) {
226
+ /** Format the queue results */
227
+ function formatResults(results, total, bailedAt, detail = 'summary') {
227
228
  const success = results.filter(r => r.status === 'success').length;
228
229
  const errors = results.filter(r => r.status === 'error').length;
229
230
  const skipped = results.filter(r => r.status === 'skipped').length;
@@ -233,11 +234,29 @@ function formatResults(results, total, bailedAt) {
233
234
  if (bailedAt >= 0) {
234
235
  lines.push(`Stopped at operation ${bailedAt + 1} due to error.`);
235
236
  }
236
- lines.push('');
237
- for (const r of results) {
238
- const icon = r.status === 'success' ? 'ok' : r.status === 'error' ? 'ERR' : 'SKIP';
239
- const summary = r.status === 'skipped' ? r.text : firstLine(r.text);
240
- lines.push(` [${r.index + 1}] ${icon}: ${summary}`);
237
+ if (detail === 'full') {
238
+ for (const r of results) {
239
+ const icon = r.status === 'success' ? 'ok' : r.status === 'error' ? 'ERR' : 'SKIP';
240
+ lines.push('');
241
+ lines.push(`---`);
242
+ lines.push(`**[${r.index + 1}] ${icon}**`);
243
+ if (r.status === 'skipped') {
244
+ lines.push(r.text);
245
+ }
246
+ else {
247
+ lines.push(stripNextSteps(r.text));
248
+ }
249
+ }
250
+ }
251
+ else {
252
+ lines.push('');
253
+ for (const r of results) {
254
+ const icon = r.status === 'success' ? 'ok' : r.status === 'error' ? 'ERR' : 'SKIP';
255
+ const summary = r.status === 'skipped' ? r.text : firstLine(r.text);
256
+ lines.push(` [${r.index + 1}] ${icon}: ${summary}`);
257
+ }
258
+ lines.push('');
259
+ lines.push('_Use `detail: "full"` for complete output from each operation._');
241
260
  }
242
261
  // Consolidated next-step: use the last successful operation's context
243
262
  const lastSuccess = [...results].reverse().find(r => r.status === 'success');
@@ -78,7 +78,7 @@ export function renderIssue(issue, transitions) {
78
78
  lines.push(`# ${issue.key}: ${issue.summary}`);
79
79
  lines.push('');
80
80
  // Core fields
81
- lines.push(`**Status:** ${formatStatus(issue.status)}`);
81
+ lines.push(`**Type:** ${issue.issueType} | **Status:** ${formatStatus(issue.status)}${issue.priority ? ` | **Priority:** ${issue.priority}` : ''}`);
82
82
  if (issue.assignee) {
83
83
  lines.push(`**Assignee:** ${issue.assignee}`);
84
84
  }
@@ -89,12 +89,39 @@ export function renderIssue(issue, transitions) {
89
89
  if (issue.parent) {
90
90
  lines.push(`**Parent:** ${issue.parent}`);
91
91
  }
92
- if (issue.dueDate) {
93
- lines.push(`**Due:** ${formatDate(issue.dueDate)}`);
94
- }
92
+ if (issue.labels && issue.labels.length > 0) {
93
+ lines.push(`**Labels:** ${issue.labels.join(', ')}`);
94
+ }
95
+ // Dates
96
+ const dates = [];
97
+ if (issue.created)
98
+ dates.push(`Created ${formatDate(issue.created)}`);
99
+ if (issue.updated)
100
+ dates.push(`Updated ${formatDate(issue.updated)}`);
101
+ if (dates.length > 0)
102
+ lines.push(`**${dates.join(' | ')}**`);
103
+ const scheduleDates = [];
104
+ if (issue.startDate)
105
+ scheduleDates.push(`Start: ${formatDate(issue.startDate)}`);
106
+ if (issue.dueDate)
107
+ scheduleDates.push(`Due: ${formatDate(issue.dueDate)}`);
108
+ if (issue.resolutionDate)
109
+ scheduleDates.push(`Resolved: ${formatDate(issue.resolutionDate)}`);
110
+ if (scheduleDates.length > 0)
111
+ lines.push(`**${scheduleDates.join(' | ')}**`);
95
112
  if (issue.storyPoints) {
96
113
  lines.push(`**Points:** ${issue.storyPoints}`);
97
114
  }
115
+ if (issue.timeEstimate) {
116
+ const hours = Math.floor(issue.timeEstimate / 3600);
117
+ const minutes = Math.floor((issue.timeEstimate % 3600) / 60);
118
+ const parts = [];
119
+ if (hours > 0)
120
+ parts.push(`${hours}h`);
121
+ if (minutes > 0)
122
+ parts.push(`${minutes}m`);
123
+ lines.push(`**Estimate:** ${parts.length > 0 ? parts.join(' ') : '0m'}`);
124
+ }
98
125
  if (issue.resolution) {
99
126
  lines.push(`**Resolution:** ${issue.resolution}`);
100
127
  }
@@ -183,10 +210,17 @@ export function renderIssueSearchResults(issues, pagination, jql) {
183
210
  const issue = issues[i];
184
211
  const num = pagination.startAt + i + 1;
185
212
  lines.push(`## ${num}. ${issue.key}: ${issue.summary}`);
186
- lines.push(`${formatStatus(issue.status)} | ${issue.assignee || 'Unassigned'}`);
187
- if (issue.dueDate) {
188
- lines.push(`Due: ${formatDate(issue.dueDate)}`);
189
- }
213
+ const meta = [`${formatStatus(issue.status)}`, issue.assignee || 'Unassigned'];
214
+ if (issue.priority)
215
+ meta.push(issue.priority);
216
+ lines.push(meta.join(' | '));
217
+ const searchDates = [];
218
+ if (issue.dueDate)
219
+ searchDates.push(`Due: ${formatDate(issue.dueDate)}`);
220
+ if (issue.startDate)
221
+ searchDates.push(`Start: ${formatDate(issue.startDate)}`);
222
+ if (searchDates.length > 0)
223
+ lines.push(searchDates.join(' | '));
190
224
  if (issue.description) {
191
225
  const desc = stripHtml(issue.description);
192
226
  if (desc.length > 0) {
@@ -188,6 +188,10 @@ export const toolSchemas = {
188
188
  type: 'object',
189
189
  description: 'Custom field values as key-value pairs.',
190
190
  },
191
+ dueDate: {
192
+ type: ['string', 'null'],
193
+ description: 'Due date in ISO format (e.g., "2025-06-15") or null to clear. For create and update.',
194
+ },
191
195
  parent: {
192
196
  type: ['string', 'null'],
193
197
  description: 'Parent issue key (e.g., PROJ-100) or null to remove.',
@@ -349,8 +353,14 @@ export const toolSchemas = {
349
353
  },
350
354
  required: ['tool', 'args'],
351
355
  },
352
- description: 'Ordered list of operations to execute (max 10).',
353
- maxItems: 10,
356
+ description: 'Ordered list of operations to execute (max 16).',
357
+ maxItems: 16,
358
+ },
359
+ detail: {
360
+ type: 'string',
361
+ enum: ['full', 'summary'],
362
+ description: 'Result detail level. summary (default): one-line status per operation. full: complete output matching individual tool calls. Use full when summary lacks needed detail.',
363
+ default: 'summary',
354
364
  },
355
365
  },
356
366
  required: ['operations'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronsb/jira-cloud-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "mcpName": "io.github.aaronsb/jira-cloud",
5
5
  "description": "Model Context Protocol (MCP) server for Jira Cloud - enables AI assistants to interact with Jira",
6
6
  "type": "module",