@aaronsb/jira-cloud-mcp 0.5.2 → 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/README.md +10 -5
- package/build/client/jira-client.js +7 -1
- package/build/docs/tool-documentation.js +1 -1
- package/build/handlers/analysis-handler.js +84 -19
- package/build/handlers/resource-handlers.js +54 -55
- package/build/index.js +16 -1
- package/build/mcp/markdown-renderer.js +61 -83
- 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
package/README.md
CHANGED
|
@@ -40,26 +40,31 @@ Generate an API token at [Atlassian Account Settings](https://id.atlassian.com/m
|
|
|
40
40
|
|
|
41
41
|
| Tool | Description |
|
|
42
42
|
|------|-------------|
|
|
43
|
-
| `manage_jira_issue` | Get, create, update, delete, move, transition, comment on, or
|
|
43
|
+
| `manage_jira_issue` | Get, create, update, delete, move, transition, comment on, link, or traverse hierarchy of issues |
|
|
44
44
|
| `manage_jira_filter` | Search for issues using JQL queries, or manage saved filters |
|
|
45
|
-
| `manage_jira_project` | List projects or get project
|
|
45
|
+
| `manage_jira_project` | List projects or get project configuration and metadata |
|
|
46
46
|
| `manage_jira_board` | List boards or get board details and configuration |
|
|
47
47
|
| `manage_jira_sprint` | Manage sprints: create, start, close, and assign issues to sprints |
|
|
48
|
-
| `queue_jira_operations` |
|
|
48
|
+
| `queue_jira_operations` | Batch multiple operations with result references (`$0.key`) and error strategies |
|
|
49
|
+
| `analyze_jira_issues` | Compute metrics, exact counts, and data cube analysis over issues selected by JQL |
|
|
49
50
|
|
|
50
|
-
Each tool accepts an `operation` parameter (except `queue_jira_operations` which takes an `operations` array).
|
|
51
|
+
Each tool accepts an `operation` parameter (except `queue_jira_operations` which takes an `operations` array, and `analyze_jira_issues` which takes `jql` + `metrics`). Per-tool documentation is available as MCP resources at `jira://tools/{tool_name}/documentation`.
|
|
52
|
+
|
|
53
|
+
See [docs/tools.md](docs/tools.md) for detailed tool descriptions, workspace patterns, and design principles.
|
|
51
54
|
|
|
52
55
|
## MCP Resources
|
|
53
56
|
|
|
54
57
|
| Resource | Description |
|
|
55
58
|
|----------|-------------|
|
|
56
59
|
| `jira://instance/summary` | Instance-level statistics |
|
|
60
|
+
| `jira://projects/distribution` | Project distribution overview |
|
|
57
61
|
| `jira://projects/{key}/overview` | Project overview with status counts |
|
|
58
62
|
| `jira://boards/{id}/overview` | Board overview with sprint info |
|
|
59
63
|
| `jira://issue-link-types` | Available issue link types |
|
|
60
64
|
| `jira://custom-fields` | Custom field catalog (auto-discovered at startup) |
|
|
61
65
|
| `jira://custom-fields/{project}/{issueType}` | Context-specific custom fields |
|
|
62
|
-
| `jira://
|
|
66
|
+
| `jira://analysis/recipes` | Analysis query patterns and compute DSL reference |
|
|
67
|
+
| `jira://tools/{name}/documentation` | Per-tool documentation |
|
|
63
68
|
|
|
64
69
|
## License
|
|
65
70
|
|
|
@@ -8,6 +8,10 @@ export class JiraClient {
|
|
|
8
8
|
get v3Client() {
|
|
9
9
|
return this.client;
|
|
10
10
|
}
|
|
11
|
+
/** Expose custom field IDs for JQL construction outside the client */
|
|
12
|
+
get customFieldIds() {
|
|
13
|
+
return this.customFields;
|
|
14
|
+
}
|
|
11
15
|
constructor(config) {
|
|
12
16
|
const clientConfig = {
|
|
13
17
|
host: config.host,
|
|
@@ -80,6 +84,7 @@ export class JiraClient {
|
|
|
80
84
|
'created',
|
|
81
85
|
'updated',
|
|
82
86
|
'resolutiondate',
|
|
87
|
+
'statuscategorychangedate',
|
|
83
88
|
'duedate',
|
|
84
89
|
this.customFields.startDate,
|
|
85
90
|
this.customFields.storyPoints,
|
|
@@ -106,6 +111,7 @@ export class JiraClient {
|
|
|
106
111
|
created: fields?.created || '',
|
|
107
112
|
updated: fields?.updated || '',
|
|
108
113
|
resolutionDate: fields?.resolutiondate || null,
|
|
114
|
+
statusCategoryChanged: fields?.statuscategorychangedate ?? fields?.statuscategorychangeddate ?? null,
|
|
109
115
|
dueDate: fields?.duedate || null,
|
|
110
116
|
startDate: fields?.[this.customFields.startDate] || null,
|
|
111
117
|
storyPoints: fields?.[this.customFields.storyPoints] ?? null,
|
|
@@ -427,7 +433,7 @@ export class JiraClient {
|
|
|
427
433
|
const leanFields = [
|
|
428
434
|
'summary', 'issuetype', 'priority', 'assignee', 'reporter',
|
|
429
435
|
'status', 'resolution', 'labels', 'created', 'updated',
|
|
430
|
-
'resolutiondate', 'duedate', 'timeestimate',
|
|
436
|
+
'resolutiondate', 'statuscategorychangedate', 'duedate', 'timeestimate',
|
|
431
437
|
this.customFields.startDate, this.customFields.storyPoints,
|
|
432
438
|
];
|
|
433
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, 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
|
}
|
|
@@ -248,17 +290,39 @@ export function removeProjectClause(jql) {
|
|
|
248
290
|
.replace(/\s*AND\s*$/i, '')
|
|
249
291
|
.trim();
|
|
250
292
|
}
|
|
293
|
+
/** Run async tasks in batches to avoid API rate limiting */
|
|
294
|
+
async function batchParallel(tasks, batchSize) {
|
|
295
|
+
const results = [];
|
|
296
|
+
for (let i = 0; i < tasks.length; i += batchSize) {
|
|
297
|
+
const batch = tasks.slice(i, i + batchSize);
|
|
298
|
+
results.push(...await Promise.all(batch.map(fn => fn())));
|
|
299
|
+
}
|
|
300
|
+
return results;
|
|
301
|
+
}
|
|
302
|
+
const ROW_BATCH_SIZE = 3; // ~18-33 concurrent count queries per batch
|
|
251
303
|
/** Build a scoped JQL by adding a condition to the base query */
|
|
252
304
|
function scopeJql(baseJql, condition) {
|
|
253
305
|
return `(${baseJql}) AND ${condition}`;
|
|
254
306
|
}
|
|
255
|
-
/** Implicit measures — lazily resolved if referenced in compute expressions
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
307
|
+
/** Implicit measures — lazily resolved if referenced in compute expressions.
|
|
308
|
+
* Built dynamically because some use custom field IDs. */
|
|
309
|
+
function buildImplicitMeasures(customFieldIds) {
|
|
310
|
+
const measures = {
|
|
311
|
+
bugs: 'issuetype = Bug AND resolution = Unresolved',
|
|
312
|
+
unassigned: 'assignee is EMPTY AND resolution = Unresolved',
|
|
313
|
+
no_due_date: 'dueDate is EMPTY AND resolution = Unresolved',
|
|
314
|
+
blocked: 'status = Blocked',
|
|
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',
|
|
319
|
+
};
|
|
320
|
+
if (customFieldIds) {
|
|
321
|
+
measures.no_estimate = `${customFieldIds.storyPoints} is EMPTY AND resolution = Unresolved`;
|
|
322
|
+
measures.no_start_date = `${customFieldIds.startDate} is EMPTY AND resolution = Unresolved`;
|
|
323
|
+
}
|
|
324
|
+
return measures;
|
|
325
|
+
}
|
|
262
326
|
/** Map dimension values to JQL clauses for groupBy scoping */
|
|
263
327
|
export function groupByJqlClause(dimension, values) {
|
|
264
328
|
switch (dimension) {
|
|
@@ -272,7 +336,7 @@ export function groupByJqlClause(dimension, values) {
|
|
|
272
336
|
return values.map(v => `issuetype = "${v}"`);
|
|
273
337
|
}
|
|
274
338
|
}
|
|
275
|
-
async function buildCountRow(jiraClient, label, baseJql, implicitMeasureNames) {
|
|
339
|
+
async function buildCountRow(jiraClient, label, baseJql, implicitMeasureNames, implicitMeasureDefs) {
|
|
276
340
|
const [total, unresolved, overdue, highPriority, createdRecently, resolvedRecently] = await Promise.all([
|
|
277
341
|
jiraClient.countIssues(baseJql),
|
|
278
342
|
jiraClient.countIssues(scopeJql(baseJql, 'resolution = Unresolved')),
|
|
@@ -282,8 +346,8 @@ async function buildCountRow(jiraClient, label, baseJql, implicitMeasureNames) {
|
|
|
282
346
|
jiraClient.countIssues(scopeJql(baseJql, 'resolved >= -7d')),
|
|
283
347
|
]);
|
|
284
348
|
let implicitMeasures;
|
|
285
|
-
if (implicitMeasureNames && implicitMeasureNames.length > 0) {
|
|
286
|
-
const counts = await Promise.all(implicitMeasureNames.map(name => jiraClient.countIssues(scopeJql(baseJql,
|
|
349
|
+
if (implicitMeasureNames && implicitMeasureNames.length > 0 && implicitMeasureDefs) {
|
|
350
|
+
const counts = await Promise.all(implicitMeasureNames.map(name => jiraClient.countIssues(scopeJql(baseJql, implicitMeasureDefs[name]))));
|
|
287
351
|
implicitMeasures = {};
|
|
288
352
|
for (let i = 0; i < implicitMeasureNames.length; i++) {
|
|
289
353
|
implicitMeasures[implicitMeasureNames[i]] = counts[i];
|
|
@@ -348,8 +412,9 @@ async function handleSummary(jiraClient, jql, groupBy, compute) {
|
|
|
348
412
|
const lines = [];
|
|
349
413
|
lines.push(`# Summary: ${jql}`);
|
|
350
414
|
lines.push(`As of ${formatDateShort(new Date())} — counts are exact (no sampling cap)`);
|
|
351
|
-
//
|
|
352
|
-
const
|
|
415
|
+
// Build implicit measures with custom field IDs for JQL construction
|
|
416
|
+
const implicitDefs = buildImplicitMeasures(jiraClient.customFieldIds);
|
|
417
|
+
const neededImplicits = compute ? detectImplicitMeasures(compute, implicitDefs) : [];
|
|
353
418
|
const groupBudget = maxGroupsForBudget(neededImplicits.length);
|
|
354
419
|
if (groupBy === 'project') {
|
|
355
420
|
let keys = extractProjectKeys(jql);
|
|
@@ -360,7 +425,7 @@ async function handleSummary(jiraClient, jql, groupBy, compute) {
|
|
|
360
425
|
if (capped)
|
|
361
426
|
keys = keys.slice(0, groupBudget);
|
|
362
427
|
const remaining = removeProjectClause(jql);
|
|
363
|
-
const rows = await
|
|
428
|
+
const rows = await batchParallel(keys.map(k => () => buildCountRow(jiraClient, k, remaining ? `project = ${k} AND (${remaining})` : `project = ${k}`, neededImplicits, implicitDefs)), ROW_BATCH_SIZE);
|
|
364
429
|
rows.sort((a, b) => b.unresolved - a.unresolved);
|
|
365
430
|
lines.push('');
|
|
366
431
|
lines.push(renderSummaryTable(rows, compute));
|
|
@@ -382,7 +447,7 @@ async function handleSummary(jiraClient, jql, groupBy, compute) {
|
|
|
382
447
|
// Cap groups to query budget
|
|
383
448
|
const cappedValues = dim.values.slice(0, groupBudget);
|
|
384
449
|
const jqlClause = groupByJqlClause(groupBy, cappedValues);
|
|
385
|
-
const rows = await
|
|
450
|
+
const rows = await batchParallel(cappedValues.map((value, idx) => () => buildCountRow(jiraClient, value, `(${jql}) AND ${jqlClause[idx]}`, neededImplicits, implicitDefs)), ROW_BATCH_SIZE);
|
|
386
451
|
rows.sort((a, b) => b.unresolved - a.unresolved);
|
|
387
452
|
lines.push('');
|
|
388
453
|
lines.push(renderSummaryTable(rows, compute));
|
|
@@ -395,16 +460,16 @@ async function handleSummary(jiraClient, jql, groupBy, compute) {
|
|
|
395
460
|
}
|
|
396
461
|
else {
|
|
397
462
|
// No groupBy — single row for the whole JQL
|
|
398
|
-
const row = await buildCountRow(jiraClient, 'All', jql, neededImplicits);
|
|
463
|
+
const row = await buildCountRow(jiraClient, 'All', jql, neededImplicits, implicitDefs);
|
|
399
464
|
lines.push('');
|
|
400
465
|
lines.push(renderSummaryTable([row], compute));
|
|
401
466
|
}
|
|
402
467
|
return lines.join('\n');
|
|
403
468
|
}
|
|
404
469
|
/** Detect which implicit measures are referenced by compute expressions */
|
|
405
|
-
function detectImplicitMeasures(compute) {
|
|
470
|
+
function detectImplicitMeasures(compute, implicitMeasureDefs) {
|
|
406
471
|
const refs = extractColumnRefs(compute);
|
|
407
|
-
return Object.keys(
|
|
472
|
+
return Object.keys(implicitMeasureDefs).filter(name => refs.has(name));
|
|
408
473
|
}
|
|
409
474
|
/** Extract distinct dimension values from sampled issues */
|
|
410
475
|
export function extractDimensions(issues) {
|
|
@@ -451,7 +516,7 @@ export function renderCubeSetup(jql, sampleSize, dimensions) {
|
|
|
451
516
|
lines.push('- total, open, overdue, high+, created_7d, resolved_7d');
|
|
452
517
|
lines.push('');
|
|
453
518
|
lines.push('Implicit measures (lazily resolved if referenced in `compute`):');
|
|
454
|
-
lines.push('- bugs, unassigned, no_due_date, blocked');
|
|
519
|
+
lines.push('- bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked, stale, stale_status, backlog_rot');
|
|
455
520
|
// Suggested cubes with cost estimates
|
|
456
521
|
lines.push('');
|
|
457
522
|
lines.push(`## Suggested Cubes (budget: ${MAX_COUNT_QUERIES} queries)`);
|
|
@@ -619,7 +684,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
619
684
|
}
|
|
620
685
|
}
|
|
621
686
|
// Next steps
|
|
622
|
-
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);
|
|
623
688
|
lines.push(nextSteps);
|
|
624
689
|
return {
|
|
625
690
|
content: [{
|
|
@@ -371,103 +371,102 @@ async function getContextCustomFields(jiraClient, projectKey, issueType) {
|
|
|
371
371
|
function getAnalysisRecipes() {
|
|
372
372
|
const markdown = `# Analysis Query Recipes
|
|
373
373
|
|
|
374
|
-
##
|
|
374
|
+
## Foundation Rules
|
|
375
375
|
|
|
376
|
-
|
|
376
|
+
- **Always start with cube_setup** on unfamiliar JQL before committing to a query. It tells you what dimensions exist, how many distinct values each has, and estimates query cost. Think of it as DESCRIBE TABLE before a SQL query.
|
|
377
|
+
- **\`metrics: ["summary"] + groupBy: "project"\` is your default opening move.** It's exact (no sampling cap), fast, and gives you a cross-project comparison in one call. Use everything else to drill down after.
|
|
378
|
+
- **The \`distribution\` metric is rich but capped at 500 issues.** Only use it scoped to a single project, never cross-project. Mixing it into a broad query will silently undersample.
|
|
379
|
+
- **\`manage_jira_project\` issue counts cap at 100** — never use it for actual counts, only for metadata. \`analyze_jira_issues\` with \`metrics: ["summary"]\` is always more accurate.
|
|
377
380
|
|
|
378
|
-
|
|
379
|
-
2. **Detail layer** — \`analyze_jira_issues\` with other metrics, or \`manage_jira_filter\` execute_jql → individual issues
|
|
381
|
+
## Core Recipes
|
|
380
382
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
### Project Health Snapshot
|
|
383
|
+
### Org-wide Health Snapshot
|
|
384
384
|
**Question:** Which projects are busiest / most at-risk?
|
|
385
385
|
\`\`\`json
|
|
386
|
-
{ "jql": "project in (
|
|
386
|
+
{ "jql": "project in (...) AND resolution = Unresolved", "metrics": ["summary"], "groupBy": "project", "compute": ["overdue_pct = overdue / open * 100", "high_pct = high / open * 100", "clearing = resolved_7d > created_7d", "planning_gap = no_due_date / open * 100"] }
|
|
387
387
|
\`\`\`
|
|
388
|
+
One query, full picture. The \`clearing\` boolean immediately flags which projects are accumulating debt.
|
|
388
389
|
|
|
389
390
|
### Net Flow / Velocity
|
|
390
391
|
**Question:** Are we resolving faster than creating?
|
|
391
|
-
Run two summary queries and subtract:
|
|
392
|
-
- \`created >= -7d\` with \`groupBy: "project"\` → created this week
|
|
393
|
-
- \`resolved >= -7d\` with \`groupBy: "project"\` → resolved this week
|
|
394
|
-
Net = created - resolved. Positive = growing backlog. Negative = clearing.
|
|
395
|
-
|
|
396
|
-
### Bug Ratio
|
|
397
|
-
**Question:** How much open work is bugs vs planned?
|
|
398
392
|
\`\`\`json
|
|
399
|
-
{ "jql": "
|
|
393
|
+
{ "jql": "project in (...) AND (created >= -7d OR resolved >= -7d)", "metrics": ["summary"], "groupBy": "project", "compute": ["net_flow = resolved_7d - created_7d", "clearing = resolved_7d > created_7d"] }
|
|
400
394
|
\`\`\`
|
|
395
|
+
Positive net_flow = clearing, negative = accumulating. The OR condition in JQL ensures you capture both sides of the ledger.
|
|
401
396
|
|
|
402
|
-
###
|
|
403
|
-
**Question:**
|
|
397
|
+
### Team Workload Scorecard
|
|
398
|
+
**Question:** Who is overloaded or under-tracked?
|
|
404
399
|
\`\`\`json
|
|
405
|
-
{ "jql": "resolution = Unresolved AND
|
|
400
|
+
{ "jql": "project in (...) AND resolution = Unresolved AND assignee is not EMPTY", "metrics": ["summary"], "groupBy": "assignee", "compute": ["overdue_pct = overdue / open * 100", "high_pct = high / open * 100", "no_dates = no_due_date / open * 100"] }
|
|
406
401
|
\`\`\`
|
|
402
|
+
The \`no_dates\` column is the secret ingredient — it distinguishes "this person is behind" from "this person has no dates set so overdue looks artificially low." Scope to 2-3 projects max.
|
|
407
403
|
|
|
408
|
-
###
|
|
409
|
-
**Question:**
|
|
404
|
+
### Planning Gap Detector
|
|
405
|
+
**Question:** Where is risk invisible?
|
|
410
406
|
\`\`\`json
|
|
411
|
-
{ "jql": "resolution = Unresolved AND dueDate
|
|
407
|
+
{ "jql": "resolution = Unresolved AND dueDate is EMPTY", "metrics": ["summary"], "groupBy": "project" }
|
|
412
408
|
\`\`\`
|
|
409
|
+
Projects with high-priority open issues and no due dates aren't "on time" — they're untracked. Don't trust overdue numbers in projects with high planning gap.
|
|
413
410
|
|
|
414
|
-
### Stale Backlog
|
|
415
|
-
**Question:**
|
|
411
|
+
### Stale Backlog Grooming Target
|
|
412
|
+
**Question:** What needs a decision?
|
|
416
413
|
\`\`\`json
|
|
417
414
|
{ "jql": "status = Backlog AND created <= -90d", "metrics": ["summary"], "groupBy": "project" }
|
|
418
415
|
\`\`\`
|
|
416
|
+
Anything sitting in Backlog 90+ days with no movement is a decision waiting to happen. Pair with \`manage_jira_filter\` execute_jql to get the actual list for a grooming session.
|
|
419
417
|
|
|
420
|
-
###
|
|
421
|
-
**Question:** What
|
|
418
|
+
### Unowned + Urgent (Danger Combo)
|
|
419
|
+
**Question:** What's high priority with no owner?
|
|
422
420
|
\`\`\`json
|
|
423
|
-
{ "jql": "resolution = Unresolved AND assignee is EMPTY", "metrics": ["summary"], "groupBy": "project" }
|
|
421
|
+
{ "jql": "resolution = Unresolved AND assignee is EMPTY AND priority in (High, Highest)", "metrics": ["summary"], "groupBy": "project" }
|
|
424
422
|
\`\`\`
|
|
423
|
+
This should always return zero. When it doesn't, it's the most actionable finding in the entire system.
|
|
425
424
|
|
|
426
|
-
###
|
|
427
|
-
**Question:**
|
|
425
|
+
### Blocked Issue Sweep
|
|
426
|
+
**Question:** What's stuck?
|
|
427
|
+
Use \`manage_jira_filter\` with \`execute_jql\` for this one:
|
|
428
428
|
\`\`\`json
|
|
429
|
-
{ "
|
|
429
|
+
{ "operation": "execute_jql", "jql": "status = Blocked ORDER BY created ASC" }
|
|
430
430
|
\`\`\`
|
|
431
|
+
Blocked lists tend to be small but each one is a potential cascade. Oldest first surfaces the longest-stuck items for escalation.
|
|
431
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
|
+
|
|
440
|
+
## Data Cube
|
|
433
441
|
|
|
434
442
|
For multi-dimensional analysis, use the two-phase cube pattern:
|
|
435
443
|
|
|
436
444
|
### Phase 1: Discover dimensions
|
|
437
445
|
\`\`\`json
|
|
438
|
-
{ "jql": "project in (
|
|
446
|
+
{ "jql": "project in (...) AND resolution = Unresolved", "metrics": ["cube_setup"] }
|
|
439
447
|
\`\`\`
|
|
440
|
-
Returns available dimensions, their values,
|
|
448
|
+
Returns available dimensions, their values, cost estimates, and query budget.
|
|
441
449
|
|
|
442
450
|
### Phase 2: Execute with computed columns
|
|
443
451
|
\`\`\`json
|
|
444
|
-
{ "jql": "project in (
|
|
452
|
+
{ "jql": "project in (...) AND resolution = Unresolved", "metrics": ["summary"], "groupBy": "project", "compute": ["bug_pct = bugs / total * 100", "net_flow = created_7d - resolved_7d", "clearing = resolved_7d > created_7d"] }
|
|
445
453
|
\`\`\`
|
|
446
454
|
|
|
447
|
-
### Compute DSL
|
|
448
|
-
- Arithmetic: \`+\`, \`-\`, \`*\`, \`/\`
|
|
449
|
-
- Comparisons: \`>\`, \`<\`, \`>=\`, \`<=\`, \`==\`, \`!=\` (produce Yes/No)
|
|
455
|
+
### Compute DSL Reference
|
|
456
|
+
- Arithmetic: \`+\`, \`-\`, \`*\`, \`/\` (division by zero = 0)
|
|
457
|
+
- Comparisons: \`>\`, \`<\`, \`>=\`, \`<=\`, \`==\`, \`!=\` (produce Yes/No — cannot be summed or averaged)
|
|
450
458
|
- Standard columns: total, open, overdue, high, created_7d, resolved_7d
|
|
451
|
-
- Implicit measures (lazily resolved): bugs, unassigned, no_due_date, blocked
|
|
452
|
-
- Max 5 expressions per query
|
|
453
|
-
|
|
454
|
-
### Example computed columns
|
|
455
|
-
- \`bug_pct = bugs / total * 100\` — bug ratio as percentage
|
|
456
|
-
- \`net_flow = created_7d - resolved_7d\` — positive = growing backlog
|
|
457
|
-
- \`clearing = resolved_7d > created_7d\` — Yes/No: is backlog shrinking?
|
|
458
|
-
- \`risk = overdue > 10\` — Yes/No flag for at-risk projects
|
|
459
|
-
- \`velocity = resolved_7d / 7\` — daily throughput
|
|
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+)
|
|
460
|
+
- Max 5 expressions per query, 150-query budget per execution
|
|
461
|
+
- Expressions evaluate linearly — later expressions can reference earlier ones
|
|
460
462
|
|
|
461
|
-
##
|
|
463
|
+
## Gotchas
|
|
462
464
|
|
|
463
|
-
-
|
|
464
|
-
-
|
|
465
|
-
-
|
|
466
|
-
-
|
|
467
|
-
- \`
|
|
468
|
-
- \`assignee is EMPTY AND priority in (High, Highest)\` = high-priority work with no owner (most actionable)
|
|
469
|
-
- Use \`cube_setup\` first to discover dimensions, then \`summary\` + \`groupBy\` + \`compute\` to execute
|
|
470
|
-
- Use \`summary\` for cross-project scope, then \`distribution\`/\`schedule\` per-project for detail
|
|
465
|
+
- **\`groupBy: "assignee"\` on large JQL will error.** Scope to 2-3 projects at a time.
|
|
466
|
+
- **Don't trust overdue in projects with high planning_gap.** If 100% of issues have no due date, overdue = 0 is meaningless, not good news.
|
|
467
|
+
- **Boolean computed columns (Yes/No) can't be summed or averaged.** Don't try to build a ratio on top of one.
|
|
468
|
+
- **\`resolved >= -7d\` is the reliable resolution window.** The Resolved 7d column in summary derives from this same window.
|
|
469
|
+
- **Use \`cube_setup\` first** to discover dimensions, then \`summary\` + \`groupBy\` + \`compute\` to execute.
|
|
471
470
|
`;
|
|
472
471
|
return {
|
|
473
472
|
contents: [{
|
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));
|
|
@@ -83,41 +83,33 @@ export function renderIssue(issue, transitions) {
|
|
|
83
83
|
const lines = [];
|
|
84
84
|
lines.push(`# ${issue.key}: ${issue.summary}`);
|
|
85
85
|
lines.push('');
|
|
86
|
-
// Core fields
|
|
87
|
-
|
|
88
|
-
if (issue.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (issue.
|
|
96
|
-
lines.push(
|
|
97
|
-
|
|
98
|
-
if (issue.labels && issue.labels.length > 0) {
|
|
99
|
-
lines.push(`**Labels:** ${issue.labels.join(', ')}`);
|
|
100
|
-
}
|
|
101
|
-
// Dates
|
|
86
|
+
// Core fields — pipe-delimited
|
|
87
|
+
const core = [issue.issueType, formatStatus(issue.status)];
|
|
88
|
+
if (issue.priority)
|
|
89
|
+
core.push(issue.priority);
|
|
90
|
+
core.push(issue.assignee || 'Unassigned');
|
|
91
|
+
lines.push(core.join(' | '));
|
|
92
|
+
lines.push(`Reporter: ${issue.reporter}`);
|
|
93
|
+
if (issue.parent)
|
|
94
|
+
lines.push(`Parent: ${issue.parent}`);
|
|
95
|
+
if (issue.labels && issue.labels.length > 0)
|
|
96
|
+
lines.push(`Labels: ${issue.labels.join(', ')}`);
|
|
97
|
+
// Dates — single line
|
|
102
98
|
const dates = [];
|
|
103
99
|
if (issue.created)
|
|
104
100
|
dates.push(`Created ${formatDate(issue.created)}`);
|
|
105
101
|
if (issue.updated)
|
|
106
102
|
dates.push(`Updated ${formatDate(issue.updated)}`);
|
|
107
|
-
if (dates.length > 0)
|
|
108
|
-
lines.push(`**${dates.join(' | ')}**`);
|
|
109
|
-
const scheduleDates = [];
|
|
110
103
|
if (issue.startDate)
|
|
111
|
-
|
|
104
|
+
dates.push(`Start ${formatDate(issue.startDate)}`);
|
|
112
105
|
if (issue.dueDate)
|
|
113
|
-
|
|
106
|
+
dates.push(`Due ${formatDate(issue.dueDate)}`);
|
|
114
107
|
if (issue.resolutionDate)
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
lines.push(
|
|
118
|
-
if (issue.storyPoints)
|
|
119
|
-
lines.push(
|
|
120
|
-
}
|
|
108
|
+
dates.push(`Resolved ${formatDate(issue.resolutionDate)}`);
|
|
109
|
+
if (dates.length > 0)
|
|
110
|
+
lines.push(dates.join(' | '));
|
|
111
|
+
if (issue.storyPoints)
|
|
112
|
+
lines.push(`Points: ${issue.storyPoints}`);
|
|
121
113
|
if (issue.timeEstimate) {
|
|
122
114
|
const hours = Math.floor(issue.timeEstimate / 3600);
|
|
123
115
|
const minutes = Math.floor((issue.timeEstimate % 3600) / 60);
|
|
@@ -126,61 +118,58 @@ export function renderIssue(issue, transitions) {
|
|
|
126
118
|
parts.push(`${hours}h`);
|
|
127
119
|
if (minutes > 0)
|
|
128
120
|
parts.push(`${minutes}m`);
|
|
129
|
-
lines.push(
|
|
121
|
+
lines.push(`Estimate: ${parts.length > 0 ? parts.join(' ') : '0m'}`);
|
|
130
122
|
}
|
|
131
|
-
if (issue.resolution)
|
|
132
|
-
lines.push(
|
|
133
|
-
|
|
134
|
-
// Description (truncated for token efficiency)
|
|
123
|
+
if (issue.resolution)
|
|
124
|
+
lines.push(`Resolution: ${issue.resolution}`);
|
|
125
|
+
// Description — full for single issue view
|
|
135
126
|
if (issue.description) {
|
|
136
127
|
lines.push('');
|
|
137
|
-
lines.push('
|
|
138
|
-
|
|
139
|
-
lines.push(truncate(desc, 300));
|
|
128
|
+
lines.push('Description:');
|
|
129
|
+
lines.push(stripHtml(issue.description));
|
|
140
130
|
}
|
|
141
131
|
// Issue links
|
|
142
132
|
if (issue.issueLinks && issue.issueLinks.length > 0) {
|
|
143
133
|
lines.push('');
|
|
144
|
-
lines.push('
|
|
134
|
+
lines.push('Links:');
|
|
145
135
|
for (const link of issue.issueLinks) {
|
|
146
|
-
if (link.outward)
|
|
147
|
-
lines.push(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
lines.push(`- ${link.type} <- ${link.inward}`);
|
|
151
|
-
}
|
|
136
|
+
if (link.outward)
|
|
137
|
+
lines.push(`${link.type} -> ${link.outward}`);
|
|
138
|
+
if (link.inward)
|
|
139
|
+
lines.push(`${link.type} <- ${link.inward}`);
|
|
152
140
|
}
|
|
153
141
|
}
|
|
154
142
|
// Comments (if present)
|
|
155
143
|
if (issue.comments && issue.comments.length > 0) {
|
|
156
144
|
lines.push('');
|
|
157
|
-
lines.push(
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
lines.push(
|
|
145
|
+
lines.push(`Comments (${issue.comments.length}):`);
|
|
146
|
+
const recentComments = issue.comments.slice(-5);
|
|
147
|
+
const startIdx = issue.comments.length - recentComments.length + 1;
|
|
148
|
+
if (issue.comments.length > 5) {
|
|
149
|
+
lines.push(` +${issue.comments.length - 5} older comments`);
|
|
162
150
|
}
|
|
163
|
-
|
|
164
|
-
|
|
151
|
+
for (let i = 0; i < recentComments.length; i++) {
|
|
152
|
+
const comment = recentComments[i];
|
|
153
|
+
lines.push(`[${startIdx + i}/${issue.comments.length}] ${comment.author} (${formatDate(comment.created)}): ${stripHtml(comment.body)}`);
|
|
165
154
|
}
|
|
166
155
|
}
|
|
167
156
|
// Custom fields (from catalog discovery)
|
|
168
157
|
if (issue.customFieldValues && issue.customFieldValues.length > 0) {
|
|
169
158
|
lines.push('');
|
|
170
|
-
lines.push('
|
|
159
|
+
lines.push('Custom Fields:');
|
|
171
160
|
for (const cf of issue.customFieldValues) {
|
|
172
161
|
const displayValue = Array.isArray(cf.value)
|
|
173
162
|
? cf.value.join(', ')
|
|
174
163
|
: String(cf.value);
|
|
175
|
-
lines.push(
|
|
164
|
+
lines.push(`${cf.name} (${cf.type}): ${displayValue}`);
|
|
176
165
|
}
|
|
177
166
|
}
|
|
178
167
|
// Available transitions
|
|
179
168
|
if (transitions && transitions.length > 0) {
|
|
180
169
|
lines.push('');
|
|
181
|
-
lines.push('
|
|
170
|
+
lines.push('Actions:');
|
|
182
171
|
for (const t of transitions) {
|
|
183
|
-
lines.push(
|
|
172
|
+
lines.push(`${t.name} -> ${t.to.name} (id: ${t.id})`);
|
|
184
173
|
}
|
|
185
174
|
}
|
|
186
175
|
return lines.join('\n');
|
|
@@ -214,26 +203,18 @@ export function renderIssueSearchResults(issues, pagination, jql) {
|
|
|
214
203
|
// Issues list
|
|
215
204
|
for (let i = 0; i < issues.length; i++) {
|
|
216
205
|
const issue = issues[i];
|
|
217
|
-
const
|
|
218
|
-
lines.push(`## ${num}. ${issue.key}: ${issue.summary}`);
|
|
219
|
-
const meta = [`${formatStatus(issue.status)}`, issue.assignee || 'Unassigned'];
|
|
206
|
+
const meta = [issue.key, issue.summary, formatStatus(issue.status), issue.assignee || 'Unassigned'];
|
|
220
207
|
if (issue.priority)
|
|
221
208
|
meta.push(issue.priority);
|
|
222
|
-
lines.push(meta.join(' | '));
|
|
223
|
-
const searchDates = [];
|
|
224
209
|
if (issue.dueDate)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
searchDates.push(`Start: ${formatDate(issue.startDate)}`);
|
|
228
|
-
if (searchDates.length > 0)
|
|
229
|
-
lines.push(searchDates.join(' | '));
|
|
210
|
+
meta.push(`due ${formatDate(issue.dueDate)}`);
|
|
211
|
+
lines.push(meta.join(' | '));
|
|
230
212
|
if (issue.description) {
|
|
231
213
|
const desc = stripHtml(issue.description);
|
|
232
214
|
if (desc.length > 0) {
|
|
233
|
-
lines.push(
|
|
215
|
+
lines.push(` ${truncate(desc, 120)}`);
|
|
234
216
|
}
|
|
235
217
|
}
|
|
236
|
-
lines.push('');
|
|
237
218
|
}
|
|
238
219
|
// Pagination guidance
|
|
239
220
|
lines.push('---');
|
|
@@ -297,19 +278,16 @@ export function renderProjectList(projects) {
|
|
|
297
278
|
lines.push(`# Projects (${projects.length})`);
|
|
298
279
|
lines.push('');
|
|
299
280
|
for (const project of projects) {
|
|
300
|
-
|
|
301
|
-
if (project.lead)
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
if (project.description) {
|
|
305
|
-
lines.push(`> ${truncate(stripHtml(project.description), 100)}`);
|
|
306
|
-
}
|
|
281
|
+
const parts = [project.key, project.name];
|
|
282
|
+
if (project.lead)
|
|
283
|
+
parts.push(project.lead);
|
|
307
284
|
if (project.statusCounts) {
|
|
308
285
|
const total = Object.values(project.statusCounts).reduce((a, b) => a + b, 0);
|
|
309
|
-
|
|
286
|
+
parts.push(`${total} issues`);
|
|
310
287
|
}
|
|
311
|
-
lines.push('');
|
|
288
|
+
lines.push(parts.join(' | '));
|
|
312
289
|
}
|
|
290
|
+
lines.push('');
|
|
313
291
|
lines.push('---');
|
|
314
292
|
lines.push(`Tip: Use manage_jira_project with operation="get" and projectKey="KEY" for details`);
|
|
315
293
|
return lines.join('\n');
|
|
@@ -348,9 +326,9 @@ export function renderBoardList(boards) {
|
|
|
348
326
|
byType[type].push(board);
|
|
349
327
|
}
|
|
350
328
|
for (const [type, typeBoards] of Object.entries(byType)) {
|
|
351
|
-
lines.push(
|
|
329
|
+
lines.push(`${type.charAt(0).toUpperCase() + type.slice(1)} Boards:`);
|
|
352
330
|
for (const board of typeBoards) {
|
|
353
|
-
lines.push(
|
|
331
|
+
lines.push(`${board.name} | id: ${board.id}${board.projectKey ? ` | ${board.projectKey}` : ''}`);
|
|
354
332
|
}
|
|
355
333
|
lines.push('');
|
|
356
334
|
}
|
|
@@ -378,7 +356,7 @@ export function renderSprint(sprint) {
|
|
|
378
356
|
}
|
|
379
357
|
if (sprint.issues && sprint.issues.length > 0) {
|
|
380
358
|
lines.push('');
|
|
381
|
-
lines.push(
|
|
359
|
+
lines.push(`Issues (${sprint.issues.length}):`);
|
|
382
360
|
// Group by status
|
|
383
361
|
const byStatus = {};
|
|
384
362
|
for (const issue of sprint.issues) {
|
|
@@ -388,9 +366,9 @@ export function renderSprint(sprint) {
|
|
|
388
366
|
byStatus[status].push(issue);
|
|
389
367
|
}
|
|
390
368
|
for (const [status, statusIssues] of Object.entries(byStatus)) {
|
|
391
|
-
lines.push(
|
|
369
|
+
lines.push(`${status} (${statusIssues.length}):`);
|
|
392
370
|
for (const issue of statusIssues) {
|
|
393
|
-
lines.push(
|
|
371
|
+
lines.push(`${issue.key} | ${issue.summary}${issue.assignee ? ` | ${issue.assignee}` : ''}`);
|
|
394
372
|
}
|
|
395
373
|
}
|
|
396
374
|
}
|
|
@@ -431,16 +409,16 @@ export function renderFilterList(filters) {
|
|
|
431
409
|
const favorites = filters.filter(f => f.favourite);
|
|
432
410
|
const others = filters.filter(f => !f.favourite);
|
|
433
411
|
if (favorites.length > 0) {
|
|
434
|
-
lines.push('
|
|
412
|
+
lines.push('Favorites:');
|
|
435
413
|
for (const filter of favorites) {
|
|
436
|
-
lines.push(
|
|
414
|
+
lines.push(`${filter.name} | id: ${filter.id} | ${filter.owner}`);
|
|
437
415
|
}
|
|
438
416
|
lines.push('');
|
|
439
417
|
}
|
|
440
418
|
if (others.length > 0) {
|
|
441
|
-
lines.push('
|
|
419
|
+
lines.push('Other Filters:');
|
|
442
420
|
for (const filter of others) {
|
|
443
|
-
lines.push(
|
|
421
|
+
lines.push(`${filter.name} | id: ${filter.id} | ${filter.owner}`);
|
|
444
422
|
}
|
|
445
423
|
}
|
|
446
424
|
return lines.join('\n');
|
|
@@ -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, 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