@aaronsb/jira-cloud-mcp 0.5.3 → 0.5.4
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 +3 -1
- package/build/docs/tool-documentation.js +1 -1
- package/build/handlers/analysis-handler.js +47 -2
- package/build/handlers/resource-handlers.js +8 -1
- package/build/index.js +16 -1
- package/build/prompts/prompt-definitions.js +34 -0
- package/build/prompts/prompt-messages.js +111 -0
- package/build/schemas/tool-schemas.js +1 -1
- package/build/utils/next-steps.js +4 -1
- package/package.json +1 -1
|
@@ -84,6 +84,7 @@ export class JiraClient {
|
|
|
84
84
|
'created',
|
|
85
85
|
'updated',
|
|
86
86
|
'resolutiondate',
|
|
87
|
+
'statuscategorychangedate',
|
|
87
88
|
'duedate',
|
|
88
89
|
this.customFields.startDate,
|
|
89
90
|
this.customFields.storyPoints,
|
|
@@ -110,6 +111,7 @@ export class JiraClient {
|
|
|
110
111
|
created: fields?.created || '',
|
|
111
112
|
updated: fields?.updated || '',
|
|
112
113
|
resolutionDate: fields?.resolutiondate || null,
|
|
114
|
+
statusCategoryChanged: fields?.statuscategorychangedate ?? fields?.statuscategorychangeddate ?? null,
|
|
113
115
|
dueDate: fields?.duedate || null,
|
|
114
116
|
startDate: fields?.[this.customFields.startDate] || null,
|
|
115
117
|
storyPoints: fields?.[this.customFields.storyPoints] ?? null,
|
|
@@ -431,7 +433,7 @@ export class JiraClient {
|
|
|
431
433
|
const leanFields = [
|
|
432
434
|
'summary', 'issuetype', 'priority', 'assignee', 'reporter',
|
|
433
435
|
'status', 'resolution', 'labels', 'created', 'updated',
|
|
434
|
-
'resolutiondate', 'duedate', 'timeestimate',
|
|
436
|
+
'resolutiondate', 'statuscategorychangedate', 'duedate', 'timeestimate',
|
|
435
437
|
this.customFields.startDate, this.customFields.storyPoints,
|
|
436
438
|
];
|
|
437
439
|
const params = {
|
|
@@ -483,7 +483,7 @@ function generateAnalysisToolDocumentation(schema) {
|
|
|
483
483
|
},
|
|
484
484
|
compute: {
|
|
485
485
|
type: "array of strings",
|
|
486
|
-
description: "Computed columns for summary tables. Each: 'name = expr'. Arithmetic, comparisons, column refs. Implicit measures: bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked.",
|
|
486
|
+
description: "Computed columns for summary tables. Each: 'name = expr'. Arithmetic, comparisons, column refs. Implicit measures: bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked, stale, stale_status, backlog_rot.",
|
|
487
487
|
},
|
|
488
488
|
maxResults: {
|
|
489
489
|
type: "integer",
|
|
@@ -210,6 +210,48 @@ export function renderCycle(issues, now) {
|
|
|
210
210
|
.slice(0, 5);
|
|
211
211
|
const oldestStr = oldest.map(o => `${o.key} (${o.age}d)`).join(', ');
|
|
212
212
|
lines.push(`**Oldest open:** ${oldestStr}`);
|
|
213
|
+
// Staleness — how long since last update
|
|
214
|
+
const staleness = open.map(i => ({
|
|
215
|
+
key: i.key,
|
|
216
|
+
days: daysBetween(parseDate(i.updated), now),
|
|
217
|
+
}));
|
|
218
|
+
const buckets = { fresh: 0, aging: 0, stale: 0, abandoned: 0 };
|
|
219
|
+
for (const s of staleness) {
|
|
220
|
+
if (s.days < 7)
|
|
221
|
+
buckets.fresh++;
|
|
222
|
+
else if (s.days < 30)
|
|
223
|
+
buckets.aging++;
|
|
224
|
+
else if (s.days < 90)
|
|
225
|
+
buckets.stale++;
|
|
226
|
+
else
|
|
227
|
+
buckets.abandoned++;
|
|
228
|
+
}
|
|
229
|
+
lines.push(`**Staleness:** <7d: ${buckets.fresh} | 7-30d: ${buckets.aging} | 30-90d: ${buckets.stale} | 90d+: ${buckets.abandoned}`);
|
|
230
|
+
// Most stale open issues
|
|
231
|
+
const mostStale = staleness
|
|
232
|
+
.sort((a, b) => b.days - a.days)
|
|
233
|
+
.slice(0, 5);
|
|
234
|
+
if (mostStale.length > 0 && mostStale[0].days >= 30) {
|
|
235
|
+
const staleStr = mostStale.map(s => `${s.key} (${s.days}d)`).join(', ');
|
|
236
|
+
lines.push(`**Most stale:** ${staleStr}`);
|
|
237
|
+
}
|
|
238
|
+
// Status age — how long in current status
|
|
239
|
+
const withStatusAge = open.filter(i => i.statusCategoryChanged);
|
|
240
|
+
if (withStatusAge.length > 0) {
|
|
241
|
+
const statusAges = withStatusAge.map(i => daysBetween(parseDate(i.statusCategoryChanged), now));
|
|
242
|
+
const med = median(statusAges);
|
|
243
|
+
const avg = mean(statusAges);
|
|
244
|
+
lines.push(`**Status age:** median ${med.toFixed(1)} days, mean ${avg.toFixed(1)} days in current status (${withStatusAge.length} issues)`);
|
|
245
|
+
const stuck = withStatusAge
|
|
246
|
+
.map(i => ({ key: i.key, status: i.status, days: daysBetween(parseDate(i.statusCategoryChanged), now) }))
|
|
247
|
+
.filter(s => s.days >= 30)
|
|
248
|
+
.sort((a, b) => b.days - a.days)
|
|
249
|
+
.slice(0, 5);
|
|
250
|
+
if (stuck.length > 0) {
|
|
251
|
+
const stuckStr = stuck.map(s => `${s.key} ${s.status} (${s.days}d)`).join(', ');
|
|
252
|
+
lines.push(`**Stuck:** ${stuckStr}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
213
255
|
}
|
|
214
256
|
return lines.join('\n');
|
|
215
257
|
}
|
|
@@ -271,6 +313,9 @@ function buildImplicitMeasures(customFieldIds) {
|
|
|
271
313
|
no_due_date: 'dueDate is EMPTY AND resolution = Unresolved',
|
|
272
314
|
blocked: 'status = Blocked',
|
|
273
315
|
no_labels: 'labels is EMPTY AND resolution = Unresolved',
|
|
316
|
+
stale: 'resolution = Unresolved AND updated <= -60d',
|
|
317
|
+
stale_status: 'resolution = Unresolved AND statusCategoryChangedDate <= -30d',
|
|
318
|
+
backlog_rot: 'resolution = Unresolved AND dueDate is EMPTY AND assignee is EMPTY AND updated <= -60d',
|
|
274
319
|
};
|
|
275
320
|
if (customFieldIds) {
|
|
276
321
|
measures.no_estimate = `${customFieldIds.storyPoints} is EMPTY AND resolution = Unresolved`;
|
|
@@ -471,7 +516,7 @@ export function renderCubeSetup(jql, sampleSize, dimensions) {
|
|
|
471
516
|
lines.push('- total, open, overdue, high+, created_7d, resolved_7d');
|
|
472
517
|
lines.push('');
|
|
473
518
|
lines.push('Implicit measures (lazily resolved if referenced in `compute`):');
|
|
474
|
-
lines.push('- bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked');
|
|
519
|
+
lines.push('- bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked, stale, stale_status, backlog_rot');
|
|
475
520
|
// Suggested cubes with cost estimates
|
|
476
521
|
lines.push('');
|
|
477
522
|
lines.push(`## Suggested Cubes (budget: ${MAX_COUNT_QUERIES} queries)`);
|
|
@@ -639,7 +684,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
639
684
|
}
|
|
640
685
|
}
|
|
641
686
|
// Next steps
|
|
642
|
-
const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key));
|
|
687
|
+
const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key), truncated);
|
|
643
688
|
lines.push(nextSteps);
|
|
644
689
|
return {
|
|
645
690
|
content: [{
|
|
@@ -430,6 +430,13 @@ Use \`manage_jira_filter\` with \`execute_jql\` for this one:
|
|
|
430
430
|
\`\`\`
|
|
431
431
|
Blocked lists tend to be small but each one is a potential cascade. Oldest first surfaces the longest-stuck items for escalation.
|
|
432
432
|
|
|
433
|
+
### Data Quality / Backlog Rot
|
|
434
|
+
**Question:** How much of this backlog is noise?
|
|
435
|
+
\`\`\`json
|
|
436
|
+
{ "jql": "project in (...) AND resolution = Unresolved", "metrics": ["summary"], "groupBy": "project", "compute": ["stale_pct = stale / open * 100", "rot_pct = backlog_rot / open * 100"] }
|
|
437
|
+
\`\`\`
|
|
438
|
+
\`stale\` = untouched 60+ days. \`backlog_rot\` = undated + unassigned + untouched 60+ days. High rot_pct means overdue counts are misleading — the backlog is full of phantom work.
|
|
439
|
+
|
|
433
440
|
## Data Cube
|
|
434
441
|
|
|
435
442
|
For multi-dimensional analysis, use the two-phase cube pattern:
|
|
@@ -449,7 +456,7 @@ Returns available dimensions, their values, cost estimates, and query budget.
|
|
|
449
456
|
- Arithmetic: \`+\`, \`-\`, \`*\`, \`/\` (division by zero = 0)
|
|
450
457
|
- Comparisons: \`>\`, \`<\`, \`>=\`, \`<=\`, \`==\`, \`!=\` (produce Yes/No — cannot be summed or averaged)
|
|
451
458
|
- Standard columns: total, open, overdue, high, created_7d, resolved_7d
|
|
452
|
-
- Implicit measures (lazily resolved via count API): bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked
|
|
459
|
+
- Implicit measures (lazily resolved via count API): bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked, stale (untouched 60d+), stale_status (stuck in status 30d+), backlog_rot (undated+unassigned+untouched 60d+)
|
|
453
460
|
- Max 5 expressions per query, 150-query budget per execution
|
|
454
461
|
- Expressions evaluate linearly — later expressions can reference earlier ones
|
|
455
462
|
|
package/build/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
3
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
-
import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { CallToolRequestSchema, ErrorCode, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import { fieldDiscovery } from './client/field-discovery.js';
|
|
7
7
|
import { JiraClient } from './client/jira-client.js';
|
|
8
8
|
import { handleAnalysisRequest } from './handlers/analysis-handler.js';
|
|
@@ -13,6 +13,8 @@ import { handleProjectRequest } from './handlers/project-handlers.js';
|
|
|
13
13
|
import { createQueueHandler } from './handlers/queue-handler.js';
|
|
14
14
|
import { setupResourceHandlers } from './handlers/resource-handlers.js';
|
|
15
15
|
import { handleSprintRequest } from './handlers/sprint-handlers.js';
|
|
16
|
+
import { promptDefinitions } from './prompts/prompt-definitions.js';
|
|
17
|
+
import { getPrompt } from './prompts/prompt-messages.js';
|
|
16
18
|
import { toolSchemas } from './schemas/tool-schemas.js';
|
|
17
19
|
// Jira credentials from environment variables
|
|
18
20
|
const JIRA_EMAIL = process.env.JIRA_EMAIL;
|
|
@@ -43,6 +45,7 @@ class JiraServer {
|
|
|
43
45
|
capabilities: {
|
|
44
46
|
tools: {},
|
|
45
47
|
resources: {},
|
|
48
|
+
prompts: {},
|
|
46
49
|
},
|
|
47
50
|
});
|
|
48
51
|
this.jiraClient = new JiraClient({
|
|
@@ -80,6 +83,18 @@ class JiraServer {
|
|
|
80
83
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
81
84
|
return resourceHandlers.readResource(request.params.uri);
|
|
82
85
|
});
|
|
86
|
+
// Set up prompt handlers
|
|
87
|
+
this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
88
|
+
prompts: promptDefinitions.map(p => ({
|
|
89
|
+
name: p.name,
|
|
90
|
+
description: p.description,
|
|
91
|
+
arguments: p.arguments,
|
|
92
|
+
})),
|
|
93
|
+
}));
|
|
94
|
+
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
95
|
+
const { name, arguments: args } = request.params;
|
|
96
|
+
return getPrompt(name, args);
|
|
97
|
+
});
|
|
83
98
|
// Set up tool handlers
|
|
84
99
|
this.server.setRequestHandler(CallToolRequestSchema, async (request, _extra) => {
|
|
85
100
|
console.error('Received request:', JSON.stringify(request, null, 2));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Prompt definitions for Jira analysis workflows.
|
|
3
|
+
* Prompts are user-controlled templates surfaced by clients as slash commands or menu items.
|
|
4
|
+
*/
|
|
5
|
+
export const promptDefinitions = [
|
|
6
|
+
{
|
|
7
|
+
name: 'backlog_health',
|
|
8
|
+
description: 'Run a data quality health check on a project backlog — surfaces rot, staleness, and planning gaps',
|
|
9
|
+
arguments: [
|
|
10
|
+
{ name: 'project', description: 'Jira project key (e.g. PROJ, ENG)', required: true },
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'contributor_workload',
|
|
15
|
+
description: 'Per-contributor workload breakdown with staleness and risk — scopes detail queries to fit within sample cap',
|
|
16
|
+
arguments: [
|
|
17
|
+
{ name: 'project', description: 'Jira project key (e.g. PROJ, ENG)', required: true },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'sprint_review',
|
|
22
|
+
description: 'Sprint review preparation — velocity, scope changes, at-risk items, and completion forecast',
|
|
23
|
+
arguments: [
|
|
24
|
+
{ name: 'board_id', description: 'Jira board ID (find via manage_jira_board list)', required: true },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'narrow_analysis',
|
|
29
|
+
description: 'Refine a capped analysis query — guides you to narrow JQL for precise detail metrics',
|
|
30
|
+
arguments: [
|
|
31
|
+
{ name: 'jql', description: 'The JQL query to refine (from a previous capped analysis)', required: true },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds PromptMessage arrays for each prompt, substituting user-provided arguments.
|
|
3
|
+
*/
|
|
4
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { promptDefinitions } from './prompt-definitions.js';
|
|
6
|
+
function msg(text) {
|
|
7
|
+
return { role: 'user', content: { type: 'text', text } };
|
|
8
|
+
}
|
|
9
|
+
const builders = {
|
|
10
|
+
backlog_health({ project }) {
|
|
11
|
+
return {
|
|
12
|
+
description: `Backlog health check for ${project}`,
|
|
13
|
+
messages: [msg(`Analyze backlog health for project ${project}. Use the analyze_jira_issues and manage_jira_filter tools.
|
|
14
|
+
|
|
15
|
+
Step 1 — Summary with data quality signals:
|
|
16
|
+
{"jql":"project = ${project} AND resolution = Unresolved","metrics":["summary"],"groupBy":"priority","compute":["rot_pct = backlog_rot / open * 100","stale_pct = stale / open * 100","gap_pct = no_estimate / open * 100"]}
|
|
17
|
+
|
|
18
|
+
Step 2 — Cycle metrics for staleness distribution and status age:
|
|
19
|
+
{"jql":"project = ${project} AND resolution = Unresolved","metrics":["cycle"],"maxResults":100}
|
|
20
|
+
|
|
21
|
+
Step 3 — Summarize findings:
|
|
22
|
+
- What percentage of the backlog is rotting (no owner, no dates, untouched)?
|
|
23
|
+
- What's stuck in the same status for 30+ days?
|
|
24
|
+
- What's missing estimates or start dates?
|
|
25
|
+
- Flag the worst offenders by issue key.
|
|
26
|
+
- Recommend specific triage actions.`)],
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
contributor_workload({ project }) {
|
|
30
|
+
return {
|
|
31
|
+
description: `Contributor workload for ${project}`,
|
|
32
|
+
messages: [msg(`Analyze contributor workload for project ${project}. Use the analyze_jira_issues tool.
|
|
33
|
+
|
|
34
|
+
Step 1 — Assignee distribution with quality signals:
|
|
35
|
+
{"jql":"project = ${project} AND resolution = Unresolved","metrics":["summary"],"groupBy":"assignee","compute":["stale_pct = stale / open * 100","blocked_pct = blocked / open * 100"]}
|
|
36
|
+
|
|
37
|
+
Step 2 — For the top 3 assignees by open issue count, run scoped detail metrics:
|
|
38
|
+
{"jql":"project = ${project} AND resolution = Unresolved AND assignee = '{name}'","metrics":["cycle","schedule"]}
|
|
39
|
+
|
|
40
|
+
This pattern keeps each detail query within the sample cap for precise results.
|
|
41
|
+
|
|
42
|
+
Step 3 — Summarize:
|
|
43
|
+
- Who has the most open work?
|
|
44
|
+
- Who has the most stale or at-risk issues?
|
|
45
|
+
- Are there load imbalances?
|
|
46
|
+
- What needs reassignment or triage?`)],
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
sprint_review({ board_id }) {
|
|
50
|
+
return {
|
|
51
|
+
description: `Sprint review prep for board ${board_id}`,
|
|
52
|
+
messages: [msg(`Prepare a sprint review for board ${board_id}. Use manage_jira_sprint and analyze_jira_issues tools.
|
|
53
|
+
|
|
54
|
+
Step 1 — Find the active sprint:
|
|
55
|
+
{"operation":"list","boardId":${board_id},"state":"active"}
|
|
56
|
+
|
|
57
|
+
Step 2 — Analyze sprint issues (use the sprint ID from step 1):
|
|
58
|
+
{"jql":"sprint = {sprintId}","metrics":["summary","points","schedule"],"compute":["done_pct = resolved_7d / total * 100"]}
|
|
59
|
+
|
|
60
|
+
Step 3 — Summarize:
|
|
61
|
+
- Current velocity vs planned
|
|
62
|
+
- Scope changes (items added/removed mid-sprint)
|
|
63
|
+
- At-risk items (overdue, blocked, stale)
|
|
64
|
+
- Completion forecast — will the sprint goal be met?`)],
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
narrow_analysis({ jql }) {
|
|
68
|
+
return {
|
|
69
|
+
description: 'Refine a capped analysis query',
|
|
70
|
+
messages: [msg(`The previous analysis was sampled — detail metrics didn't cover all matching issues.
|
|
71
|
+
|
|
72
|
+
Original query: ${jql}
|
|
73
|
+
|
|
74
|
+
To get precise results, help me narrow the query. Here are useful approaches:
|
|
75
|
+
|
|
76
|
+
By assignee (each person's list usually fits within the cap):
|
|
77
|
+
{"jql":"${jql} AND assignee = currentUser()","metrics":["cycle","schedule"]}
|
|
78
|
+
|
|
79
|
+
By priority (focus on what matters):
|
|
80
|
+
{"jql":"${jql} AND priority in (High, Highest)","metrics":["cycle","schedule"]}
|
|
81
|
+
|
|
82
|
+
By issue type:
|
|
83
|
+
{"jql":"${jql} AND issuetype = Bug","metrics":["cycle"]}
|
|
84
|
+
|
|
85
|
+
By recency:
|
|
86
|
+
{"jql":"${jql} AND created >= -30d","metrics":["cycle"]}
|
|
87
|
+
|
|
88
|
+
Or use summary metrics for the full population (count API, no cap):
|
|
89
|
+
{"jql":"${jql}","metrics":["summary"],"groupBy":"assignee","compute":["stale_pct = stale / open * 100"]}
|
|
90
|
+
|
|
91
|
+
Ask me which dimension I'd like to drill into, or suggest the most useful one based on the original query.`)],
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
export function getPrompt(name, args) {
|
|
96
|
+
const def = promptDefinitions.find(p => p.name === name);
|
|
97
|
+
if (!def) {
|
|
98
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${name}`);
|
|
99
|
+
}
|
|
100
|
+
const resolvedArgs = args ?? {};
|
|
101
|
+
for (const arg of def.arguments) {
|
|
102
|
+
if (arg.required && !resolvedArgs[arg.name]) {
|
|
103
|
+
throw new McpError(ErrorCode.InvalidParams, `Missing required argument: ${arg.name}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const builder = builders[name];
|
|
107
|
+
if (!builder) {
|
|
108
|
+
throw new Error(`No message builder for prompt: ${name}`);
|
|
109
|
+
}
|
|
110
|
+
return builder(resolvedArgs);
|
|
111
|
+
}
|
|
@@ -350,7 +350,7 @@ export const toolSchemas = {
|
|
|
350
350
|
compute: {
|
|
351
351
|
type: 'array',
|
|
352
352
|
items: { type: 'string' },
|
|
353
|
-
description: 'Computed columns for cube execute. Each entry: "name = expr". Arithmetic (+,-,*,/), comparisons (>,<,>=,<=,==,!=). Column refs: total, open, overdue, high, created_7d, resolved_7d. Implicit measures resolved lazily: bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked. Max 5 expressions. Example: ["bug_pct = bugs / total * 100", "
|
|
353
|
+
description: 'Computed columns for cube execute. Each entry: "name = expr". Arithmetic (+,-,*,/), comparisons (>,<,>=,<=,==,!=). Column refs: total, open, overdue, high, created_7d, resolved_7d. Implicit measures resolved lazily: bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked, stale (untouched 60d+), stale_status (stuck in status 30d+), backlog_rot (undated+unassigned+untouched 60d+). Max 5 expressions. Example: ["bug_pct = bugs / total * 100", "rot_pct = backlog_rot / open * 100"].',
|
|
354
354
|
maxItems: 5,
|
|
355
355
|
},
|
|
356
356
|
maxResults: {
|
|
@@ -125,11 +125,14 @@ export function boardNextSteps(operation, boardId) {
|
|
|
125
125
|
}
|
|
126
126
|
return steps.length > 0 ? formatSteps(steps) : '';
|
|
127
127
|
}
|
|
128
|
-
export function analysisNextSteps(jql, issueKeys) {
|
|
128
|
+
export function analysisNextSteps(jql, issueKeys, truncated = false) {
|
|
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
133
|
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
|
+
if (truncated) {
|
|
135
|
+
steps.push({ description: 'Detail metrics are sampled — narrow JQL by assignee, priority, or type for precise results. Use summary metrics (count API) for whole-project totals', tool: 'analyze_jira_issues', example: { jql: `${jql} AND assignee = currentUser()`, metrics: ['cycle'] } });
|
|
136
|
+
}
|
|
134
137
|
return formatSteps(steps) + '\n- Read `jira://analysis/recipes` for data cube patterns and compute DSL examples';
|
|
135
138
|
}
|
package/package.json
CHANGED