@aaronsb/jira-cloud-mcp 0.5.9 → 0.5.10
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.
|
@@ -410,6 +410,11 @@ export class JiraClient {
|
|
|
410
410
|
url: attachment.content || '',
|
|
411
411
|
}));
|
|
412
412
|
}
|
|
413
|
+
async getFilter(filterId) {
|
|
414
|
+
return await this.client.filters.getFilter({
|
|
415
|
+
id: parseInt(filterId, 10),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
413
418
|
async getFilterIssues(filterId) {
|
|
414
419
|
const filter = await this.client.filters.getFilter({
|
|
415
420
|
id: parseInt(filterId, 10)
|
|
@@ -592,9 +592,29 @@ async function handleCubeSetup(jiraClient, jql) {
|
|
|
592
592
|
// ── Main Handler ───────────────────────────────────────────────────────
|
|
593
593
|
export async function handleAnalysisRequest(jiraClient, request) {
|
|
594
594
|
const args = normalizeArgs(request.params?.arguments || {});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
595
|
+
// Resolve JQL: filterId takes precedence over inline jql
|
|
596
|
+
let jql;
|
|
597
|
+
let filterSource;
|
|
598
|
+
const filterId = args.filterId;
|
|
599
|
+
if (filterId && typeof filterId === 'string' && filterId.trim() !== '') {
|
|
600
|
+
let filter;
|
|
601
|
+
try {
|
|
602
|
+
filter = await jiraClient.getFilter(filterId);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
throw new McpError(ErrorCode.InvalidParams, `Filter ${filterId} not found or not accessible. Use manage_jira_filter with operation "list" to see available filters.`);
|
|
606
|
+
}
|
|
607
|
+
if (!filter.jql) {
|
|
608
|
+
throw new McpError(ErrorCode.InvalidParams, `Filter ${filterId} has no JQL query.`);
|
|
609
|
+
}
|
|
610
|
+
jql = filter.jql;
|
|
611
|
+
filterSource = `${filter.name || filterId} (filter ${filterId})`;
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
jql = args.jql;
|
|
615
|
+
if (!jql || typeof jql !== 'string' || jql.trim() === '') {
|
|
616
|
+
throw new McpError(ErrorCode.InvalidParams, 'Either jql or filterId parameter is required.');
|
|
617
|
+
}
|
|
598
618
|
}
|
|
599
619
|
// Parse requested metrics
|
|
600
620
|
const requested = (args.metrics && Array.isArray(args.metrics))
|
|
@@ -620,22 +640,24 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
620
640
|
// Cube setup — discover dimensions from sample, no issue fetching
|
|
621
641
|
if (hasCubeSetup) {
|
|
622
642
|
const cubeText = await handleCubeSetup(jiraClient, jql);
|
|
623
|
-
const nextSteps = analysisNextSteps(jql, []);
|
|
643
|
+
const nextSteps = analysisNextSteps(jql, [], false, undefined, filterSource);
|
|
644
|
+
const banner = filterSource ? `*Using saved filter: ${filterSource}*\n\n` : '';
|
|
624
645
|
return {
|
|
625
646
|
content: [{
|
|
626
647
|
type: 'text',
|
|
627
|
-
text: cubeText + '\n' + nextSteps,
|
|
648
|
+
text: banner + cubeText + '\n' + nextSteps,
|
|
628
649
|
}],
|
|
629
650
|
};
|
|
630
651
|
}
|
|
631
652
|
// If only summary requested, skip issue fetching entirely
|
|
632
653
|
if (hasSummary && fetchMetrics.length === 0) {
|
|
633
654
|
const summaryText = await handleSummary(jiraClient, jql, groupBy, compute, groupLimit);
|
|
634
|
-
const nextSteps = analysisNextSteps(jql, [], false, groupBy);
|
|
655
|
+
const nextSteps = analysisNextSteps(jql, [], false, groupBy, filterSource);
|
|
656
|
+
const banner = filterSource ? `*Using saved filter: ${filterSource}*\n\n` : '';
|
|
635
657
|
return {
|
|
636
658
|
content: [{
|
|
637
659
|
type: 'text',
|
|
638
|
-
text: summaryText + '\n' + nextSteps,
|
|
660
|
+
text: banner + summaryText + '\n' + nextSteps,
|
|
639
661
|
}],
|
|
640
662
|
};
|
|
641
663
|
}
|
|
@@ -676,6 +698,10 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
676
698
|
}
|
|
677
699
|
const now = new Date();
|
|
678
700
|
const lines = [];
|
|
701
|
+
if (filterSource) {
|
|
702
|
+
lines.push(`*Using saved filter: ${filterSource}*`);
|
|
703
|
+
lines.push('');
|
|
704
|
+
}
|
|
679
705
|
// Summary first if requested alongside other metrics
|
|
680
706
|
if (hasSummary) {
|
|
681
707
|
const summaryText = await handleSummary(jiraClient, jql, groupBy, compute, groupLimit);
|
|
@@ -704,7 +730,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
704
730
|
}
|
|
705
731
|
}
|
|
706
732
|
// Next steps
|
|
707
|
-
const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key), truncated, groupBy);
|
|
733
|
+
const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key), truncated, groupBy, filterSource);
|
|
708
734
|
lines.push(nextSteps);
|
|
709
735
|
return {
|
|
710
736
|
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 — Cycle metrics for staleness distribution:
|
|
23
|
+
{"tool":"analyze_jira_issues","args":{"filterId":"$0.filterId","metrics":["cycle"],"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,13 +326,17 @@ 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) only when you need per-issue analysis; these are capped at maxResults issues. Always prefer this tool over manage_jira_filter or manage_jira_project for quantitative questions. Read jira://analysis/recipes for composition patterns.',
|
|
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) only when you need per-issue analysis; these are capped at maxResults issues. Always prefer this tool over manage_jira_filter or manage_jira_project for quantitative questions. 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',
|
|
@@ -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