@aaronsb/jira-cloud-mcp 0.5.6 → 0.5.8

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.
@@ -134,7 +134,7 @@ export class JiraClient {
134
134
  people: people.length > 0 ? people : undefined,
135
135
  };
136
136
  }
137
- async getIssue(issueKey, includeComments = false, includeAttachments = false, customFieldMeta) {
137
+ async getIssue(issueKey, includeComments = false, includeAttachments = false, customFieldMeta, includeHistory = false) {
138
138
  const fields = [...this.issueFields];
139
139
  if (includeAttachments) {
140
140
  fields.push('attachment');
@@ -147,13 +147,32 @@ export class JiraClient {
147
147
  }
148
148
  }
149
149
  }
150
+ const expands = [];
151
+ if (includeComments)
152
+ expands.push('comments');
153
+ if (includeHistory)
154
+ expands.push('changelog');
150
155
  const params = {
151
156
  issueIdOrKey: issueKey,
152
157
  fields,
153
- expand: includeComments ? 'comments' : undefined
158
+ expand: expands.length > 0 ? expands.join(',') : undefined,
154
159
  };
155
160
  const issue = await this.client.issues.getIssue(params);
156
161
  const issueDetails = this.mapIssueFields(issue);
162
+ // Extract status transitions from changelog
163
+ if (includeHistory && issue.changelog?.histories) {
164
+ const histories = issue.changelog.histories;
165
+ issueDetails.statusHistory = histories
166
+ .flatMap(h => h.items
167
+ .filter(item => item.field === 'status')
168
+ .map(item => ({
169
+ date: h.created,
170
+ from: item.fromString || '',
171
+ to: item.toString || '',
172
+ author: h.author?.displayName || 'Unknown',
173
+ })))
174
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
175
+ }
157
176
  // Extract custom field values using catalog metadata
158
177
  if (customFieldMeta) {
159
178
  const rawFields = issue.fields;
@@ -422,11 +422,9 @@ async function handleSummary(jiraClient, jql, groupBy, compute, groupLimit = DEF
422
422
  const groupBudget = maxGroupsForBudget(neededImplicits.length);
423
423
  // Effective group cap: user's preference vs API query budget (whichever is smaller)
424
424
  const effectiveGroupCap = Math.min(groupLimit, groupBudget);
425
- if (groupBy === 'project') {
425
+ if (groupBy === 'project' && extractProjectKeys(jql).length > 0) {
426
+ // Fast path: project keys are explicit in JQL — no sampling needed
426
427
  let keys = extractProjectKeys(jql);
427
- if (keys.length === 0) {
428
- throw new McpError(ErrorCode.InvalidParams, 'groupBy "project" requires project keys in JQL (e.g., project in (AA, GC))');
429
- }
430
428
  const capped = keys.length > effectiveGroupCap;
431
429
  if (capped)
432
430
  keys = keys.slice(0, effectiveGroupCap);
@@ -479,6 +477,11 @@ async function handleSummary(jiraClient, jql, groupBy, compute, groupLimit = DEF
479
477
  lines.push('');
480
478
  lines.push(renderSummaryTable([row], compute));
481
479
  }
480
+ // Workload interpretation hint — steer LLMs to decompose before reporting raw numbers
481
+ if (groupBy === 'assignee') {
482
+ lines.push('');
483
+ lines.push('*Interpretation tip: High open counts per person may include backlog, review, and future-planned work — not just active tasks. Before reporting workload, break down by status: `metrics: ["summary"], groupBy: "issuetype"` scoped to one assignee to distinguish active work from queued/backlog items.*');
484
+ }
482
485
  return lines.join('\n');
483
486
  }
484
487
  /** Detect which implicit measures are referenced by compute expressions */
@@ -628,7 +631,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
628
631
  // If only summary requested, skip issue fetching entirely
629
632
  if (hasSummary && fetchMetrics.length === 0) {
630
633
  const summaryText = await handleSummary(jiraClient, jql, groupBy, compute, groupLimit);
631
- const nextSteps = analysisNextSteps(jql, []);
634
+ const nextSteps = analysisNextSteps(jql, [], false, groupBy);
632
635
  return {
633
636
  content: [{
634
637
  type: 'text',
@@ -701,7 +704,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
701
704
  }
702
705
  }
703
706
  // Next steps
704
- const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key), truncated);
707
+ const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key), truncated, groupBy);
705
708
  lines.push(nextSteps);
706
709
  return {
707
710
  content: [{
@@ -187,7 +187,8 @@ async function handleGetIssue(jiraClient, args) {
187
187
  // Get issue with requested expansions and catalog custom fields
188
188
  const includeComments = expansionOptions.comments || false;
189
189
  const includeAttachments = expansionOptions.attachments || false;
190
- const issue = await jiraClient.getIssue(args.issueKey, includeComments, includeAttachments, getCatalogFieldMeta());
190
+ const includeHistory = expansionOptions.history || false;
191
+ const issue = await jiraClient.getIssue(args.issueKey, includeComments, includeAttachments, getCatalogFieldMeta(), includeHistory);
191
192
  // Get transitions if requested
192
193
  let transitions = undefined;
193
194
  if (expansionOptions.transitions) {
@@ -165,6 +165,20 @@ export function renderIssue(issue, transitions) {
165
165
  lines.push(`${cf.name} (${cf.type}): ${displayValue}`);
166
166
  }
167
167
  }
168
+ // Status history (if requested via expand: ["history"])
169
+ if (issue.statusHistory && issue.statusHistory.length > 0) {
170
+ lines.push('');
171
+ lines.push('Status History:');
172
+ for (const h of issue.statusHistory) {
173
+ lines.push(`${formatDate(h.date)}: ${h.from} → ${h.to} (by ${h.author})`);
174
+ }
175
+ // Show time in current status
176
+ const last = issue.statusHistory[issue.statusHistory.length - 1];
177
+ const daysSince = Math.floor((Date.now() - new Date(last.date).getTime()) / (1000 * 60 * 60 * 24));
178
+ if (daysSince > 0) {
179
+ lines.push(`*In "${last.to}" for ${daysSince} days*`);
180
+ }
181
+ }
168
182
  // Available transitions
169
183
  if (transitions && transitions.length > 0) {
170
184
  lines.push('');
@@ -125,11 +125,21 @@ export function boardNextSteps(operation, boardId) {
125
125
  }
126
126
  return steps.length > 0 ? formatSteps(steps) : '';
127
127
  }
128
- export function analysisNextSteps(jql, issueKeys, truncated = false) {
128
+ export function analysisNextSteps(jql, issueKeys, truncated = false, groupBy) {
129
129
  const steps = [];
130
130
  if (issueKeys.length > 0) {
131
131
  steps.push({ description: 'Get details on a specific issue', tool: 'manage_jira_issue', example: { operation: 'get', issueKey: issueKeys[0] } });
132
132
  }
133
+ // Contextual cross-dimension suggestions based on groupBy
134
+ if (groupBy === 'assignee') {
135
+ steps.push({ description: 'Break down a person\'s workload by status (active vs backlog vs review)', tool: 'analyze_jira_issues', example: { jql: `${jql} AND assignee = "<name>"`, metrics: ['summary'], groupBy: 'issuetype' } }, { description: 'Compare workload health with computed metrics', tool: 'analyze_jira_issues', example: { jql, metrics: ['summary'], groupBy: 'assignee', compute: ['overdue_pct = overdue / open * 100', 'stale_pct = stale / open * 100'] } });
136
+ }
137
+ else if (groupBy === 'issuetype' || groupBy === 'priority') {
138
+ steps.push({ description: 'See who owns these issues', tool: 'analyze_jira_issues', example: { jql, metrics: ['summary'], groupBy: 'assignee' } });
139
+ }
140
+ else if (groupBy === 'project') {
141
+ steps.push({ description: 'Drill into a project by assignee', tool: 'analyze_jira_issues', example: { jql: `${jql} AND project = <KEY>`, metrics: ['summary'], groupBy: 'assignee' } });
142
+ }
133
143
  steps.push({ description: 'Discover dimensions for cube analysis', tool: 'analyze_jira_issues', example: { jql, metrics: ['cube_setup'] } }, { description: 'Add computed columns', tool: 'analyze_jira_issues', example: { jql, metrics: ['summary'], groupBy: 'project', compute: ['bug_pct = bugs / total * 100'] } }, { description: 'Narrow the analysis with refined JQL', tool: 'analyze_jira_issues', example: { jql: `${jql} AND priority = High` } }, { description: 'View the full issue list', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql } });
134
144
  if (truncated) {
135
145
  steps.push({ description: 'Distribution counts above are approximate (issue cap hit). For exact breakdowns use summary + groupBy', tool: 'analyze_jira_issues', example: { jql, metrics: ['summary'], groupBy: 'assignee' } }, { description: 'Or narrow JQL for precise detail metrics', tool: 'analyze_jira_issues', example: { jql: `${jql} AND assignee = currentUser()`, metrics: ['cycle'] } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronsb/jira-cloud-mcp",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
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",