@aaronsb/jira-cloud-mcp 0.5.9 → 0.5.11
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/build/client/jira-client.js +38 -0
- package/build/docs/tool-documentation.js +8 -0
- package/build/handlers/analysis-handler.js +140 -11
- package/build/prompts/prompt-messages.js +33 -18
- package/build/schemas/tool-schemas.js +10 -6
- package/build/utils/next-steps.js +6 -2
- package/package.json +1 -1
|
@@ -104,6 +104,7 @@ export class JiraClient {
|
|
|
104
104
|
people.push({ displayName: fields.reporter.displayName, accountId: fields.reporter.accountId, role: 'reporter' });
|
|
105
105
|
}
|
|
106
106
|
return {
|
|
107
|
+
id: issue.id,
|
|
107
108
|
key: issue.key,
|
|
108
109
|
summary: fields?.summary,
|
|
109
110
|
description: fields?.description
|
|
@@ -410,6 +411,43 @@ export class JiraClient {
|
|
|
410
411
|
url: attachment.content || '',
|
|
411
412
|
}));
|
|
412
413
|
}
|
|
414
|
+
async getBulkChangelogs(issueKeys, fieldIds = ['status']) {
|
|
415
|
+
const result = new Map();
|
|
416
|
+
let nextPageToken;
|
|
417
|
+
do {
|
|
418
|
+
const response = await this.client.issues.getBulkChangelogs({
|
|
419
|
+
issueIdsOrKeys: issueKeys,
|
|
420
|
+
fieldIds,
|
|
421
|
+
maxResults: 1000,
|
|
422
|
+
nextPageToken,
|
|
423
|
+
});
|
|
424
|
+
for (const issueLog of response.issueChangeLogs || []) {
|
|
425
|
+
const issueId = issueLog.issueId;
|
|
426
|
+
if (!issueId)
|
|
427
|
+
continue;
|
|
428
|
+
const transitions = result.get(issueId) || [];
|
|
429
|
+
for (const history of issueLog.changeHistories || []) {
|
|
430
|
+
for (const item of history.items || []) {
|
|
431
|
+
if (item.field === 'status') {
|
|
432
|
+
transitions.push({
|
|
433
|
+
date: history.created || '',
|
|
434
|
+
from: item.fromString || '',
|
|
435
|
+
to: item.toString || '',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
result.set(issueId, transitions);
|
|
441
|
+
}
|
|
442
|
+
nextPageToken = response.nextPageToken ?? undefined;
|
|
443
|
+
} while (nextPageToken);
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
async getFilter(filterId) {
|
|
447
|
+
return await this.client.filters.getFilter({
|
|
448
|
+
id: parseInt(filterId, 10),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
413
451
|
async getFilterIssues(filterId) {
|
|
414
452
|
const filter = await this.client.filters.getFilter({
|
|
415
453
|
id: parseInt(filterId, 10)
|
|
@@ -528,6 +528,14 @@ function generateAnalysisToolDocumentation(schema) {
|
|
|
528
528
|
{ description: "Summary only", code: { jql: "project = AA", metrics: ["summary"] } },
|
|
529
529
|
],
|
|
530
530
|
},
|
|
531
|
+
{
|
|
532
|
+
title: "Flow analysis — where do issues get stuck?",
|
|
533
|
+
description: "Analyze status transitions, time in status, and bounce patterns:",
|
|
534
|
+
steps: [
|
|
535
|
+
{ description: "Flow metrics", code: { jql: "project = AA AND resolution = Unresolved", metrics: ["flow"] } },
|
|
536
|
+
{ description: "Combined with summary", code: { jql: "project = AA", metrics: ["summary", "flow"], groupBy: "issuetype" } },
|
|
537
|
+
],
|
|
538
|
+
},
|
|
531
539
|
{
|
|
532
540
|
title: "Data cube — discover then compute",
|
|
533
541
|
description: "Two-phase analysis with computed columns:",
|
|
@@ -3,6 +3,7 @@ import { evaluateRow, extractColumnRefs, parseComputeList } from '../utils/cube-
|
|
|
3
3
|
import { analysisNextSteps } from '../utils/next-steps.js';
|
|
4
4
|
import { normalizeArgs } from '../utils/normalize-args.js';
|
|
5
5
|
const ALL_METRICS = ['points', 'time', 'schedule', 'cycle', 'distribution'];
|
|
6
|
+
// flow is opt-in only — requires extra bulk changelog API call
|
|
6
7
|
const VALID_GROUP_BY = ['project', 'assignee', 'priority', 'issuetype'];
|
|
7
8
|
const MAX_ISSUES_HARD = 500; // absolute ceiling for detail metrics — beyond this, context explodes
|
|
8
9
|
const MAX_ISSUES_DEFAULT = 100;
|
|
@@ -271,6 +272,100 @@ export function renderDistribution(issues, groupLimit = DEFAULT_GROUP_LIMIT) {
|
|
|
271
272
|
lines.push(`**By type:** ${mapToString(byType, ' | ', groupLimit)}`);
|
|
272
273
|
return lines.join('\n');
|
|
273
274
|
}
|
|
275
|
+
export async function renderFlow(jiraClient, issues) {
|
|
276
|
+
if (issues.length === 0)
|
|
277
|
+
return '## Flow\n\nNo issues to analyze.';
|
|
278
|
+
const issueKeys = issues.map(i => i.key);
|
|
279
|
+
// Build a map of issue key → issue ID for matching bulk response
|
|
280
|
+
const keyToId = new Map();
|
|
281
|
+
const idToKey = new Map();
|
|
282
|
+
for (const issue of issues) {
|
|
283
|
+
if (issue.id) {
|
|
284
|
+
keyToId.set(issue.key, issue.id);
|
|
285
|
+
idToKey.set(issue.id, issue.key);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const changelogs = await jiraClient.getBulkChangelogs(issueKeys);
|
|
289
|
+
// Aggregate per-status stats
|
|
290
|
+
const statusStats = new Map();
|
|
291
|
+
const issueBounceCounts = new Map(); // issueId → status → entry count
|
|
292
|
+
let totalTransitions = 0;
|
|
293
|
+
for (const [issueId, transitions] of changelogs) {
|
|
294
|
+
if (transitions.length === 0)
|
|
295
|
+
continue;
|
|
296
|
+
totalTransitions += transitions.length;
|
|
297
|
+
// Sort transitions by date
|
|
298
|
+
const sorted = [...transitions].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
299
|
+
// Track entries per status for this issue
|
|
300
|
+
const issueStatusEntries = new Map();
|
|
301
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
302
|
+
const t = sorted[i];
|
|
303
|
+
const toStatus = t.to;
|
|
304
|
+
// Count entries into the target status
|
|
305
|
+
issueStatusEntries.set(toStatus, (issueStatusEntries.get(toStatus) || 0) + 1);
|
|
306
|
+
// Calculate time spent in the target status
|
|
307
|
+
const enteredAt = new Date(t.date).getTime();
|
|
308
|
+
const exitedAt = i + 1 < sorted.length
|
|
309
|
+
? new Date(sorted[i + 1].date).getTime()
|
|
310
|
+
: Date.now();
|
|
311
|
+
const daysIn = (exitedAt - enteredAt) / (1000 * 60 * 60 * 24);
|
|
312
|
+
const stats = statusStats.get(toStatus) || { status: toStatus, entries: 0, totalDaysIn: 0, issueCount: 0, bounces: 0 };
|
|
313
|
+
stats.entries++;
|
|
314
|
+
stats.totalDaysIn += daysIn;
|
|
315
|
+
statusStats.set(toStatus, stats);
|
|
316
|
+
}
|
|
317
|
+
issueBounceCounts.set(issueId, issueStatusEntries);
|
|
318
|
+
}
|
|
319
|
+
// Count distinct issues and bounces per status
|
|
320
|
+
const issuesPerStatus = new Map();
|
|
321
|
+
for (const [issueId, statusEntries] of issueBounceCounts) {
|
|
322
|
+
for (const [status, count] of statusEntries) {
|
|
323
|
+
if (!issuesPerStatus.has(status))
|
|
324
|
+
issuesPerStatus.set(status, new Set());
|
|
325
|
+
issuesPerStatus.get(status).add(issueId);
|
|
326
|
+
const stats = statusStats.get(status);
|
|
327
|
+
if (stats && count > 1) {
|
|
328
|
+
stats.bounces += count - 1;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const [status, issueSet] of issuesPerStatus) {
|
|
333
|
+
const stats = statusStats.get(status);
|
|
334
|
+
if (stats)
|
|
335
|
+
stats.issueCount = issueSet.size;
|
|
336
|
+
}
|
|
337
|
+
if (statusStats.size === 0) {
|
|
338
|
+
return '## Flow\n\nNo status transitions found in these issues.';
|
|
339
|
+
}
|
|
340
|
+
// Render table
|
|
341
|
+
const lines = ['## Flow (Status Transitions)', ''];
|
|
342
|
+
lines.push(`${totalTransitions} transitions across ${changelogs.size} issues`);
|
|
343
|
+
lines.push('');
|
|
344
|
+
lines.push('| Status | Entries | Avg days in | Bounce rate | Issues |');
|
|
345
|
+
lines.push('|--------|--------:|------------:|------------:|-------:|');
|
|
346
|
+
const sorted = [...statusStats.values()].sort((a, b) => b.entries - a.entries);
|
|
347
|
+
for (const s of sorted) {
|
|
348
|
+
const avgDays = s.entries > 0 ? (s.totalDaysIn / s.entries).toFixed(1) : '—';
|
|
349
|
+
const bounceRate = s.issueCount > 0 ? Math.round((s.bounces / s.issueCount) * 100) : 0;
|
|
350
|
+
lines.push(`| ${s.status} | ${s.entries} | ${avgDays} | ${bounceRate}% | ${s.issueCount} |`);
|
|
351
|
+
}
|
|
352
|
+
// Top bouncers — issues with most re-entries to any status
|
|
353
|
+
const bouncerScores = [];
|
|
354
|
+
for (const [issueId, statusEntries] of issueBounceCounts) {
|
|
355
|
+
const totalBounces = [...statusEntries.values()].reduce((sum, count) => sum + Math.max(0, count - 1), 0);
|
|
356
|
+
if (totalBounces > 0) {
|
|
357
|
+
const key = idToKey.get(issueId) || issueId;
|
|
358
|
+
bouncerScores.push({ key, bounces: totalBounces });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (bouncerScores.length > 0) {
|
|
362
|
+
bouncerScores.sort((a, b) => b.bounces - a.bounces);
|
|
363
|
+
const top = bouncerScores.slice(0, 5);
|
|
364
|
+
lines.push('');
|
|
365
|
+
lines.push(`**Top bouncers:** ${top.map(b => `${b.key} (${b.bounces}×)`).join(', ')}`);
|
|
366
|
+
}
|
|
367
|
+
return lines.join('\n');
|
|
368
|
+
}
|
|
274
369
|
/** Extract project keys from JQL like "project in (AA, GC, GD)" or "project = AA" */
|
|
275
370
|
export function extractProjectKeys(jql) {
|
|
276
371
|
// project in (AA, GC, GD)
|
|
@@ -592,9 +687,29 @@ async function handleCubeSetup(jiraClient, jql) {
|
|
|
592
687
|
// ── Main Handler ───────────────────────────────────────────────────────
|
|
593
688
|
export async function handleAnalysisRequest(jiraClient, request) {
|
|
594
689
|
const args = normalizeArgs(request.params?.arguments || {});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
690
|
+
// Resolve JQL: filterId takes precedence over inline jql
|
|
691
|
+
let jql;
|
|
692
|
+
let filterSource;
|
|
693
|
+
const filterId = args.filterId;
|
|
694
|
+
if (filterId && typeof filterId === 'string' && filterId.trim() !== '') {
|
|
695
|
+
let filter;
|
|
696
|
+
try {
|
|
697
|
+
filter = await jiraClient.getFilter(filterId);
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
throw new McpError(ErrorCode.InvalidParams, `Filter ${filterId} not found or not accessible. Use manage_jira_filter with operation "list" to see available filters.`);
|
|
701
|
+
}
|
|
702
|
+
if (!filter.jql) {
|
|
703
|
+
throw new McpError(ErrorCode.InvalidParams, `Filter ${filterId} has no JQL query.`);
|
|
704
|
+
}
|
|
705
|
+
jql = filter.jql;
|
|
706
|
+
filterSource = `${filter.name || filterId} (filter ${filterId})`;
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
jql = args.jql;
|
|
710
|
+
if (!jql || typeof jql !== 'string' || jql.trim() === '') {
|
|
711
|
+
throw new McpError(ErrorCode.InvalidParams, 'Either jql or filterId parameter is required.');
|
|
712
|
+
}
|
|
598
713
|
}
|
|
599
714
|
// Parse requested metrics
|
|
600
715
|
const requested = (args.metrics && Array.isArray(args.metrics))
|
|
@@ -602,9 +717,12 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
602
717
|
: [];
|
|
603
718
|
const hasSummary = requested.includes('summary');
|
|
604
719
|
const hasCubeSetup = requested.includes('cube_setup');
|
|
720
|
+
const hasFlow = requested.includes('flow');
|
|
605
721
|
const fetchMetrics = requested.length > 0
|
|
606
722
|
? requested.filter(m => ALL_METRICS.includes(m))
|
|
607
723
|
: ALL_METRICS;
|
|
724
|
+
// flow needs issue fetching but isn't in ALL_METRICS (opt-in only)
|
|
725
|
+
const needsIssueFetch = fetchMetrics.length > 0 || hasFlow;
|
|
608
726
|
// Parse groupBy
|
|
609
727
|
const groupBy = (typeof args.groupBy === 'string' && VALID_GROUP_BY.includes(args.groupBy))
|
|
610
728
|
? args.groupBy
|
|
@@ -620,22 +738,24 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
620
738
|
// Cube setup — discover dimensions from sample, no issue fetching
|
|
621
739
|
if (hasCubeSetup) {
|
|
622
740
|
const cubeText = await handleCubeSetup(jiraClient, jql);
|
|
623
|
-
const nextSteps = analysisNextSteps(jql, []);
|
|
741
|
+
const nextSteps = analysisNextSteps(jql, [], false, undefined, filterSource);
|
|
742
|
+
const banner = filterSource ? `*Using saved filter: ${filterSource}*\n\n` : '';
|
|
624
743
|
return {
|
|
625
744
|
content: [{
|
|
626
745
|
type: 'text',
|
|
627
|
-
text: cubeText + '\n' + nextSteps,
|
|
746
|
+
text: banner + cubeText + '\n' + nextSteps,
|
|
628
747
|
}],
|
|
629
748
|
};
|
|
630
749
|
}
|
|
631
|
-
// If only summary requested, skip issue fetching entirely
|
|
632
|
-
if (hasSummary &&
|
|
750
|
+
// If only summary requested (no flow or detail metrics), skip issue fetching entirely
|
|
751
|
+
if (hasSummary && !needsIssueFetch) {
|
|
633
752
|
const summaryText = await handleSummary(jiraClient, jql, groupBy, compute, groupLimit);
|
|
634
|
-
const nextSteps = analysisNextSteps(jql, [], false, groupBy);
|
|
753
|
+
const nextSteps = analysisNextSteps(jql, [], false, groupBy, filterSource);
|
|
754
|
+
const banner = filterSource ? `*Using saved filter: ${filterSource}*\n\n` : '';
|
|
635
755
|
return {
|
|
636
756
|
content: [{
|
|
637
757
|
type: 'text',
|
|
638
|
-
text: summaryText + '\n' + nextSteps,
|
|
758
|
+
text: banner + summaryText + '\n' + nextSteps,
|
|
639
759
|
}],
|
|
640
760
|
};
|
|
641
761
|
}
|
|
@@ -676,13 +796,17 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
676
796
|
}
|
|
677
797
|
const now = new Date();
|
|
678
798
|
const lines = [];
|
|
799
|
+
if (filterSource) {
|
|
800
|
+
lines.push(`*Using saved filter: ${filterSource}*`);
|
|
801
|
+
lines.push('');
|
|
802
|
+
}
|
|
679
803
|
// Summary first if requested alongside other metrics
|
|
680
804
|
if (hasSummary) {
|
|
681
805
|
const summaryText = await handleSummary(jiraClient, jql, groupBy, compute, groupLimit);
|
|
682
806
|
lines.push(summaryText);
|
|
683
807
|
}
|
|
684
808
|
// Header for fetch-based metrics
|
|
685
|
-
if (fetchMetrics.length > 0 && allIssues.length > 0) {
|
|
809
|
+
if ((fetchMetrics.length > 0 || hasFlow) && allIssues.length > 0) {
|
|
686
810
|
if (hasSummary)
|
|
687
811
|
lines.push('');
|
|
688
812
|
lines.push(`# Detail: ${jql}`);
|
|
@@ -703,8 +827,13 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
703
827
|
lines.push(renderers[metric]());
|
|
704
828
|
}
|
|
705
829
|
}
|
|
830
|
+
// Flow is opt-in and requires async bulk changelog fetch
|
|
831
|
+
if (hasFlow && allIssues.length > 0) {
|
|
832
|
+
lines.push('');
|
|
833
|
+
lines.push(await renderFlow(jiraClient, allIssues));
|
|
834
|
+
}
|
|
706
835
|
// Next steps
|
|
707
|
-
const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key), truncated, groupBy);
|
|
836
|
+
const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key), truncated, groupBy, filterSource);
|
|
708
837
|
lines.push(nextSteps);
|
|
709
838
|
return {
|
|
710
839
|
content: [{
|
|
@@ -10,40 +10,50 @@ const builders = {
|
|
|
10
10
|
backlog_health({ project }) {
|
|
11
11
|
return {
|
|
12
12
|
description: `Backlog health check for ${project}`,
|
|
13
|
-
messages: [msg(`Analyze backlog health for project ${project}. Use the
|
|
13
|
+
messages: [msg(`Analyze backlog health for project ${project}. Use the queue_jira_operations tool to run this as a pipeline.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Use queue_jira_operations with this pipeline:
|
|
16
|
+
Operation 0 — Save the query as a reusable filter:
|
|
17
|
+
{"tool":"manage_jira_filter","args":{"operation":"create","name":"${project} backlog health","jql":"project = ${project} AND resolution = Unresolved"}}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
{"
|
|
19
|
+
Operation 1 — Summary with data quality signals (uses $0.filterId):
|
|
20
|
+
{"tool":"analyze_jira_issues","args":{"filterId":"$0.filterId","metrics":["summary"],"groupBy":"priority","compute":["rot_pct = backlog_rot / open * 100","stale_pct = stale / open * 100","gap_pct = no_estimate / open * 100"]}}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
Operation 2 — Flow analysis for transition patterns and bottlenecks:
|
|
23
|
+
{"tool":"analyze_jira_issues","args":{"filterId":"$0.filterId","metrics":["flow"],"maxResults":100}}
|
|
24
|
+
|
|
25
|
+
After the pipeline completes, summarize findings:
|
|
22
26
|
- What percentage of the backlog is rotting (no owner, no dates, untouched)?
|
|
23
27
|
- What's stuck in the same status for 30+ days?
|
|
24
28
|
- What's missing estimates or start dates?
|
|
25
29
|
- Flag the worst offenders by issue key.
|
|
26
|
-
- Recommend specific triage actions
|
|
30
|
+
- Recommend specific triage actions.
|
|
31
|
+
- The saved filter can be reused for follow-up analysis or shared with the team.`)],
|
|
27
32
|
};
|
|
28
33
|
},
|
|
29
34
|
contributor_workload({ project }) {
|
|
30
35
|
return {
|
|
31
36
|
description: `Contributor workload for ${project}`,
|
|
32
|
-
messages: [msg(`Analyze contributor workload for project ${project}. Use the
|
|
37
|
+
messages: [msg(`Analyze contributor workload for project ${project}. Use queue_jira_operations to run the analysis pipeline in a single call.
|
|
38
|
+
|
|
39
|
+
Use queue_jira_operations with this pipeline:
|
|
40
|
+
Operation 0 — Save the base query as a filter:
|
|
41
|
+
{"tool":"manage_jira_filter","args":{"operation":"create","name":"${project} workload","jql":"project = ${project} AND resolution = Unresolved"}}
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
{"
|
|
43
|
+
Operation 1 — Assignee distribution with quality signals (uses $0.filterId):
|
|
44
|
+
{"tool":"analyze_jira_issues","args":{"filterId":"$0.filterId","metrics":["summary"],"groupBy":"assignee","compute":["stale_pct = stale / open * 100","blocked_pct = blocked / open * 100"]}}
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
After the pipeline, for the top 3 assignees by open count, run scoped detail:
|
|
38
47
|
{"jql":"project = ${project} AND resolution = Unresolved AND assignee = '{name}'","metrics":["cycle","schedule"]}
|
|
39
48
|
|
|
40
|
-
This
|
|
49
|
+
This keeps each detail query within the sample cap for precise results.
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
Summarize:
|
|
43
52
|
- Who has the most open work?
|
|
44
53
|
- Who has the most stale or at-risk issues?
|
|
45
54
|
- Are there load imbalances?
|
|
46
|
-
- What needs reassignment or triage
|
|
55
|
+
- What needs reassignment or triage?
|
|
56
|
+
- The saved filter can be reused for follow-up workload checks.`)],
|
|
47
57
|
};
|
|
48
58
|
},
|
|
49
59
|
sprint_review({ board_id }) {
|
|
@@ -52,16 +62,21 @@ Step 3 — Summarize:
|
|
|
52
62
|
messages: [msg(`Prepare a sprint review for board ${board_id}. Use manage_jira_sprint and analyze_jira_issues tools.
|
|
53
63
|
|
|
54
64
|
Step 1 — Find the active sprint:
|
|
55
|
-
{"operation":"list","boardId":${board_id},"state":"active"}
|
|
65
|
+
Use manage_jira_sprint: {"operation":"list","boardId":${board_id},"state":"active"}
|
|
66
|
+
|
|
67
|
+
Step 2 — Use queue_jira_operations to run the analysis pipeline (use the sprint ID from step 1):
|
|
68
|
+
Operation 0 — Save the sprint query as a filter:
|
|
69
|
+
{"tool":"manage_jira_filter","args":{"operation":"create","name":"Sprint review board ${board_id}","jql":"sprint = {sprintId}"}}
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
{"
|
|
71
|
+
Operation 1 — Summary + velocity metrics:
|
|
72
|
+
{"tool":"analyze_jira_issues","args":{"filterId":"$0.filterId","metrics":["summary","points","schedule"],"compute":["done_pct = resolved_7d / total * 100"]}}
|
|
59
73
|
|
|
60
74
|
Step 3 — Summarize:
|
|
61
75
|
- Current velocity vs planned
|
|
62
76
|
- Scope changes (items added/removed mid-sprint)
|
|
63
77
|
- At-risk items (overdue, blocked, stale)
|
|
64
|
-
- Completion forecast — will the sprint goal be met
|
|
78
|
+
- Completion forecast — will the sprint goal be met?
|
|
79
|
+
- The saved filter persists for daily standups or end-of-sprint reporting.`)],
|
|
65
80
|
};
|
|
66
81
|
},
|
|
67
82
|
narrow_analysis({ jql }) {
|
|
@@ -326,21 +326,25 @@ export const toolSchemas = {
|
|
|
326
326
|
},
|
|
327
327
|
analyze_jira_issues: {
|
|
328
328
|
name: 'analyze_jira_issues',
|
|
329
|
-
description: 'Compute project metrics over issues selected by JQL. For counting and breakdown questions ("how many by status/assignee/priority"), use metrics: ["summary"] with groupBy — this gives exact counts with no issue cap. Use detail metrics (points, time, schedule, cycle, distribution)
|
|
329
|
+
description: 'Compute project metrics over issues selected by JQL or a saved filter. For counting and breakdown questions ("how many by status/assignee/priority"), use metrics: ["summary"] with groupBy — this gives exact counts with no issue cap. Use detail metrics (points, time, schedule, cycle, distribution) for per-issue analysis (capped at maxResults). Use flow for status transition patterns — how issues move through statuses, where they bounce, and how long they stay. Tip: save complex JQL as a filter with manage_jira_filter, then reuse the filterId here for repeated analysis. Read jira://analysis/recipes for composition patterns.',
|
|
330
330
|
inputSchema: {
|
|
331
331
|
type: 'object',
|
|
332
332
|
properties: {
|
|
333
333
|
jql: {
|
|
334
334
|
type: 'string',
|
|
335
|
-
description: 'JQL query selecting the issues to analyze. Examples: "project in (AA, GC, LGS)", "sprint in openSprints()", "assignee = currentUser() AND resolution = Unresolved".',
|
|
335
|
+
description: 'JQL query selecting the issues to analyze. Either jql or filterId is required (filterId takes precedence). Examples: "project in (AA, GC, LGS)", "sprint in openSprints()", "assignee = currentUser() AND resolution = Unresolved".',
|
|
336
|
+
},
|
|
337
|
+
filterId: {
|
|
338
|
+
type: 'string',
|
|
339
|
+
description: 'ID of a saved Jira filter to use as the query source. The filter\'s JQL is resolved automatically. Use this to run different analyses against a saved query without repeating the JQL. Create filters with manage_jira_filter.',
|
|
336
340
|
},
|
|
337
341
|
metrics: {
|
|
338
342
|
type: 'array',
|
|
339
343
|
items: {
|
|
340
344
|
type: 'string',
|
|
341
|
-
enum: ['summary', 'points', 'time', 'schedule', 'cycle', 'distribution', 'cube_setup'],
|
|
345
|
+
enum: ['summary', 'points', 'time', 'schedule', 'cycle', 'distribution', 'flow', 'cube_setup'],
|
|
342
346
|
},
|
|
343
|
-
description: 'Which metric groups to compute. summary = exact counts via count API (no issue cap, fastest) — use with groupBy for "how many by assignee/status/priority" questions. distribution = approximate counts from fetched issues (capped by maxResults — use summary + groupBy instead when you need exact counts). cube_setup = discover dimensions before cube queries. points = earned value/SPI. time = effort estimates. schedule = overdue/risk. cycle = lead time/throughput. Default: all detail metrics. For counting/breakdown questions, always prefer summary + groupBy over distribution.',
|
|
347
|
+
description: 'Which metric groups to compute. summary = exact counts via count API (no issue cap, fastest) — use with groupBy for "how many by assignee/status/priority" questions. distribution = approximate counts from fetched issues (capped by maxResults — use summary + groupBy instead when you need exact counts). flow = status transition analysis from bulk changelogs — entries per status, avg time in each, bounce rates, top bouncers. cube_setup = discover dimensions before cube queries. points = earned value/SPI. time = effort estimates. schedule = overdue/risk. cycle = lead time/throughput. Default: all detail metrics (excluding flow — request flow explicitly). For counting/breakdown questions, always prefer summary + groupBy over distribution.',
|
|
344
348
|
},
|
|
345
349
|
groupBy: {
|
|
346
350
|
type: 'string',
|
|
@@ -364,12 +368,12 @@ export const toolSchemas = {
|
|
|
364
368
|
default: 100,
|
|
365
369
|
},
|
|
366
370
|
},
|
|
367
|
-
required: [
|
|
371
|
+
required: [],
|
|
368
372
|
},
|
|
369
373
|
},
|
|
370
374
|
queue_jira_operations: {
|
|
371
375
|
name: 'queue_jira_operations',
|
|
372
|
-
description: 'Execute multiple Jira operations in a single call. Operations run sequentially with result references ($0.key) and per-operation error strategies (bail/continue).',
|
|
376
|
+
description: 'Execute multiple Jira operations in a single call. Operations run sequentially with result references ($0.key) and per-operation error strategies (bail/continue). Powerful for analysis pipelines: create a filter, then run multiple analyze_jira_issues calls against $0.filterId with different groupBy/compute — all in one call.',
|
|
373
377
|
inputSchema: {
|
|
374
378
|
type: 'object',
|
|
375
379
|
properties: {
|
|
@@ -61,7 +61,7 @@ export function filterNextSteps(operation, filterId, jql) {
|
|
|
61
61
|
steps.push({ description: 'Run a filter', tool: 'manage_jira_filter', example: { operation: 'execute_filter', filterId: '<id>' } }, { description: 'Search with JQL directly', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: '<query>' } });
|
|
62
62
|
break;
|
|
63
63
|
case 'create':
|
|
64
|
-
steps.push({ description: 'Execute the new filter', tool: 'manage_jira_filter', example: { operation: 'execute_filter', filterId } });
|
|
64
|
+
steps.push({ description: 'Execute the new filter', tool: 'manage_jira_filter', example: { operation: 'execute_filter', filterId } }, { description: 'Run analysis against this filter', tool: 'analyze_jira_issues', example: { filterId, metrics: ['summary'], groupBy: 'assignee' } });
|
|
65
65
|
break;
|
|
66
66
|
}
|
|
67
67
|
return steps.length > 0 ? formatSteps(steps) : '';
|
|
@@ -125,7 +125,7 @@ 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, groupBy) {
|
|
128
|
+
export function analysisNextSteps(jql, issueKeys, truncated = false, groupBy, filterSource) {
|
|
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] } });
|
|
@@ -144,5 +144,9 @@ export function analysisNextSteps(jql, issueKeys, truncated = false, groupBy) {
|
|
|
144
144
|
if (truncated) {
|
|
145
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'] } });
|
|
146
146
|
}
|
|
147
|
+
// Suggest saving as filter if not already using one
|
|
148
|
+
if (!filterSource) {
|
|
149
|
+
steps.push({ description: 'Save this query as a filter for reuse across analyses', tool: 'manage_jira_filter', example: { operation: 'create', name: '<descriptive name>', jql } });
|
|
150
|
+
}
|
|
147
151
|
return formatSteps(steps) + '\n- Read `jira://analysis/recipes` for data cube patterns and compute DSL examples';
|
|
148
152
|
}
|
package/package.json
CHANGED