@aaronsb/jira-cloud-mcp 0.5.0 → 0.5.1
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.
|
@@ -408,6 +408,18 @@ export class JiraClient {
|
|
|
408
408
|
});
|
|
409
409
|
}
|
|
410
410
|
/** Lightweight search returning only fields needed for analysis (no description, links, rendered HTML) */
|
|
411
|
+
async countIssues(jql) {
|
|
412
|
+
try {
|
|
413
|
+
const cleanJql = jql.replace(/\\"/g, '"');
|
|
414
|
+
console.error(`Counting issues for JQL: ${cleanJql}`);
|
|
415
|
+
const result = await this.client.issueSearch.countIssues({ jql: cleanJql });
|
|
416
|
+
return result.count ?? 0;
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
console.error('Error counting issues:', error);
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
411
423
|
async searchIssuesLean(jql, maxResults = 50, nextPageToken) {
|
|
412
424
|
try {
|
|
413
425
|
const cleanJql = jql.replace(/\\"/g, '"');
|
|
@@ -473,15 +473,21 @@ function generateAnalysisToolDocumentation(schema) {
|
|
|
473
473
|
},
|
|
474
474
|
metrics: {
|
|
475
475
|
type: "array of strings",
|
|
476
|
-
description: "Which metric groups to
|
|
477
|
-
values: ["points", "time", "schedule", "cycle", "distribution"],
|
|
476
|
+
description: "Which metric groups to compute. summary uses count API (no cap). Others fetch issue data (subject to maxResults).",
|
|
477
|
+
values: ["summary", "points", "time", "schedule", "cycle", "distribution"],
|
|
478
|
+
},
|
|
479
|
+
groupBy: {
|
|
480
|
+
type: "string",
|
|
481
|
+
description: "Split summary counts by dimension. Use with metrics: ['summary'].",
|
|
482
|
+
values: ["project", "assignee", "priority", "issuetype"],
|
|
478
483
|
},
|
|
479
484
|
maxResults: {
|
|
480
485
|
type: "integer",
|
|
481
|
-
description: "Max issues to
|
|
486
|
+
description: "Max issues to fetch for detail metrics (default 100, max 500). Does not apply to summary.",
|
|
482
487
|
},
|
|
483
488
|
},
|
|
484
489
|
metric_groups: {
|
|
490
|
+
summary: "Exact counts — total, open, overdue, high priority, created/resolved last 7 days. No sampling cap. Supports groupBy for cross-project comparison.",
|
|
485
491
|
points: "Earned Value — PV, EV, remaining, SPI, status breakdown, unestimated count",
|
|
486
492
|
time: "Effort — original estimate, completed, remaining by status category",
|
|
487
493
|
schedule: "Risk — date window, overdue count/slip, due soon, concentration risk, missing dates",
|
|
@@ -489,6 +495,13 @@ function generateAnalysisToolDocumentation(schema) {
|
|
|
489
495
|
distribution: "Composition — counts by status, assignee, priority, issue type",
|
|
490
496
|
},
|
|
491
497
|
common_use_cases: [
|
|
498
|
+
{
|
|
499
|
+
title: "Cross-project comparison",
|
|
500
|
+
description: "Compare issue counts across multiple projects (exact, no cap):",
|
|
501
|
+
steps: [
|
|
502
|
+
{ description: "Summary by project", code: { jql: "project in (AA, GC, GD, LGS)", metrics: ["summary"], groupBy: "project" } },
|
|
503
|
+
],
|
|
504
|
+
},
|
|
492
505
|
{
|
|
493
506
|
title: "Sprint health check",
|
|
494
507
|
description: "Analyze all issues in the current sprint:",
|
|
@@ -504,10 +517,10 @@ function generateAnalysisToolDocumentation(schema) {
|
|
|
504
517
|
],
|
|
505
518
|
},
|
|
506
519
|
{
|
|
507
|
-
title: "
|
|
508
|
-
description: "
|
|
520
|
+
title: "Quick project overview",
|
|
521
|
+
description: "Get exact issue counts without fetching data:",
|
|
509
522
|
steps: [
|
|
510
|
-
{ description: "
|
|
523
|
+
{ description: "Summary only", code: { jql: "project = AA", metrics: ["summary"] } },
|
|
511
524
|
],
|
|
512
525
|
},
|
|
513
526
|
],
|
|
@@ -2,6 +2,7 @@ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
|
2
2
|
import { analysisNextSteps } from '../utils/next-steps.js';
|
|
3
3
|
import { normalizeArgs } from '../utils/normalize-args.js';
|
|
4
4
|
const ALL_METRICS = ['points', 'time', 'schedule', 'cycle', 'distribution'];
|
|
5
|
+
const VALID_GROUP_BY = ['project', 'assignee', 'priority', 'issuetype'];
|
|
5
6
|
const MAX_ISSUES = 500;
|
|
6
7
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
7
8
|
function bucketStatus(category) {
|
|
@@ -217,6 +218,89 @@ export function renderDistribution(issues) {
|
|
|
217
218
|
lines.push(`**By type:** ${mapToString(byType)}`);
|
|
218
219
|
return lines.join('\n');
|
|
219
220
|
}
|
|
221
|
+
/** Extract project keys from JQL like "project in (AA, GC, GD)" or "project = AA" */
|
|
222
|
+
export function extractProjectKeys(jql) {
|
|
223
|
+
// project in (AA, GC, GD)
|
|
224
|
+
const inMatch = jql.match(/project\s+in\s*\(([^)]+)\)/i);
|
|
225
|
+
if (inMatch) {
|
|
226
|
+
return inMatch[1].split(',').map(k => k.trim().replace(/['"]/g, ''));
|
|
227
|
+
}
|
|
228
|
+
// project = AA
|
|
229
|
+
const eqMatch = jql.match(/project\s*=\s*['"]?(\w+)['"]?/i);
|
|
230
|
+
if (eqMatch) {
|
|
231
|
+
return [eqMatch[1]];
|
|
232
|
+
}
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
/** Remove the project clause from JQL, returning remaining constraints */
|
|
236
|
+
export function removeProjectClause(jql) {
|
|
237
|
+
return jql
|
|
238
|
+
.replace(/project\s+in\s*\([^)]+\)\s*(AND\s*)?/i, '')
|
|
239
|
+
.replace(/project\s*=\s*['"]?\w+['"]?\s*(AND\s*)?/i, '')
|
|
240
|
+
.replace(/^\s*AND\s*/i, '')
|
|
241
|
+
.replace(/\s*AND\s*$/i, '')
|
|
242
|
+
.trim();
|
|
243
|
+
}
|
|
244
|
+
/** Build a scoped JQL by adding a condition to the base query */
|
|
245
|
+
function scopeJql(baseJql, condition) {
|
|
246
|
+
return `(${baseJql}) AND ${condition}`;
|
|
247
|
+
}
|
|
248
|
+
async function buildCountRow(jiraClient, label, baseJql) {
|
|
249
|
+
const [total, unresolved, overdue, highPriority, createdRecently, resolvedRecently] = await Promise.all([
|
|
250
|
+
jiraClient.countIssues(baseJql),
|
|
251
|
+
jiraClient.countIssues(scopeJql(baseJql, 'resolution = Unresolved')),
|
|
252
|
+
jiraClient.countIssues(scopeJql(baseJql, 'resolution = Unresolved AND dueDate < now()')),
|
|
253
|
+
jiraClient.countIssues(scopeJql(baseJql, 'priority in (High, Highest, Critical, Blocker)')),
|
|
254
|
+
jiraClient.countIssues(scopeJql(baseJql, 'created >= -7d')),
|
|
255
|
+
jiraClient.countIssues(scopeJql(baseJql, 'resolved >= -7d')),
|
|
256
|
+
]);
|
|
257
|
+
return { label, total, unresolved, overdue, highPriority, createdRecently, resolvedRecently };
|
|
258
|
+
}
|
|
259
|
+
export function renderSummaryTable(rows) {
|
|
260
|
+
const lines = ['## Summary (exact counts)', ''];
|
|
261
|
+
lines.push('| Scope | Total | Open | Overdue | High+ | Created 7d | Resolved 7d |');
|
|
262
|
+
lines.push('|-------|------:|-----:|--------:|------:|-----------:|------------:|');
|
|
263
|
+
for (const r of rows) {
|
|
264
|
+
lines.push(`| ${r.label} | ${r.total} | ${r.unresolved} | ${r.overdue} | ${r.highPriority} | ${r.createdRecently} | ${r.resolvedRecently} |`);
|
|
265
|
+
}
|
|
266
|
+
// Totals row if multiple
|
|
267
|
+
if (rows.length > 1) {
|
|
268
|
+
const sum = (fn) => rows.reduce((s, r) => s + fn(r), 0);
|
|
269
|
+
lines.push(`| **Total** | **${sum(r => r.total)}** | **${sum(r => r.unresolved)}** | **${sum(r => r.overdue)}** | **${sum(r => r.highPriority)}** | **${sum(r => r.createdRecently)}** | **${sum(r => r.resolvedRecently)}** |`);
|
|
270
|
+
}
|
|
271
|
+
return lines.join('\n');
|
|
272
|
+
}
|
|
273
|
+
async function handleSummary(jiraClient, jql, groupBy) {
|
|
274
|
+
const lines = [];
|
|
275
|
+
lines.push(`# Summary: ${jql}`);
|
|
276
|
+
lines.push(`As of ${formatDateShort(new Date())} — counts are exact (no sampling cap)`);
|
|
277
|
+
if (groupBy === 'project') {
|
|
278
|
+
const keys = extractProjectKeys(jql);
|
|
279
|
+
if (keys.length === 0) {
|
|
280
|
+
throw new McpError(ErrorCode.InvalidParams, 'groupBy "project" requires project keys in JQL (e.g., project in (AA, GC))');
|
|
281
|
+
}
|
|
282
|
+
const remaining = removeProjectClause(jql);
|
|
283
|
+
const rows = await Promise.all(keys.map(k => buildCountRow(jiraClient, k, remaining ? `project = ${k} AND (${remaining})` : `project = ${k}`)));
|
|
284
|
+
rows.sort((a, b) => b.unresolved - a.unresolved);
|
|
285
|
+
lines.push('');
|
|
286
|
+
lines.push(renderSummaryTable(rows));
|
|
287
|
+
}
|
|
288
|
+
else if (groupBy) {
|
|
289
|
+
// For non-project groupBy, get the overall count first
|
|
290
|
+
const overallRow = await buildCountRow(jiraClient, 'All', jql);
|
|
291
|
+
lines.push('');
|
|
292
|
+
lines.push(renderSummaryTable([overallRow]));
|
|
293
|
+
lines.push('');
|
|
294
|
+
lines.push(`*groupBy "${groupBy}" — only "project" supports per-group breakdown currently. Other dimensions coming soon.*`);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// No groupBy — single row for the whole JQL
|
|
298
|
+
const row = await buildCountRow(jiraClient, 'All', jql);
|
|
299
|
+
lines.push('');
|
|
300
|
+
lines.push(renderSummaryTable([row]));
|
|
301
|
+
}
|
|
302
|
+
return lines.join('\n');
|
|
303
|
+
}
|
|
220
304
|
// ── Main Handler ───────────────────────────────────────────────────────
|
|
221
305
|
export async function handleAnalysisRequest(jiraClient, request) {
|
|
222
306
|
const args = normalizeArgs(request.params?.arguments || {});
|
|
@@ -224,16 +308,31 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
224
308
|
if (!jql || typeof jql !== 'string' || jql.trim() === '') {
|
|
225
309
|
throw new McpError(ErrorCode.InvalidParams, 'jql parameter is required.');
|
|
226
310
|
}
|
|
227
|
-
const DEFAULT_MAX = 100;
|
|
228
|
-
const maxResults = Math.min(Number(args.maxResults) || DEFAULT_MAX, MAX_ISSUES);
|
|
229
311
|
// Parse requested metrics
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
312
|
+
const requested = (args.metrics && Array.isArray(args.metrics))
|
|
313
|
+
? args.metrics
|
|
314
|
+
: [];
|
|
315
|
+
const hasSummary = requested.includes('summary');
|
|
316
|
+
const fetchMetrics = requested.length > 0
|
|
317
|
+
? requested.filter(m => ALL_METRICS.includes(m))
|
|
318
|
+
: ALL_METRICS;
|
|
319
|
+
// Parse groupBy
|
|
320
|
+
const groupBy = (typeof args.groupBy === 'string' && VALID_GROUP_BY.includes(args.groupBy))
|
|
321
|
+
? args.groupBy
|
|
322
|
+
: undefined;
|
|
323
|
+
// If only summary requested, skip issue fetching entirely
|
|
324
|
+
if (hasSummary && fetchMetrics.length === 0) {
|
|
325
|
+
const summaryText = await handleSummary(jiraClient, jql, groupBy);
|
|
326
|
+
const nextSteps = analysisNextSteps(jql, []);
|
|
327
|
+
return {
|
|
328
|
+
content: [{
|
|
329
|
+
type: 'text',
|
|
330
|
+
text: summaryText + '\n' + nextSteps,
|
|
331
|
+
}],
|
|
332
|
+
};
|
|
236
333
|
}
|
|
334
|
+
const DEFAULT_MAX = 100;
|
|
335
|
+
const maxResults = Math.min(Number(args.maxResults) || DEFAULT_MAX, MAX_ISSUES);
|
|
237
336
|
// Fetch issues using cursor-based pagination (50 per page, Jira enhanced search API)
|
|
238
337
|
const allIssues = [];
|
|
239
338
|
const seen = new Set();
|
|
@@ -260,7 +359,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
260
359
|
break;
|
|
261
360
|
}
|
|
262
361
|
}
|
|
263
|
-
if (allIssues.length === 0) {
|
|
362
|
+
if (allIssues.length === 0 && !hasSummary) {
|
|
264
363
|
return {
|
|
265
364
|
content: [{
|
|
266
365
|
type: 'text',
|
|
@@ -270,23 +369,32 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
270
369
|
}
|
|
271
370
|
const now = new Date();
|
|
272
371
|
const lines = [];
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
lines.push(`*Results capped at ${maxResults} issues — query may match more.*`);
|
|
372
|
+
// Summary first if requested alongside other metrics
|
|
373
|
+
if (hasSummary) {
|
|
374
|
+
const summaryText = await handleSummary(jiraClient, jql, groupBy);
|
|
375
|
+
lines.push(summaryText);
|
|
278
376
|
}
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
377
|
+
// Header for fetch-based metrics
|
|
378
|
+
if (fetchMetrics.length > 0 && allIssues.length > 0) {
|
|
379
|
+
if (hasSummary)
|
|
380
|
+
lines.push('');
|
|
381
|
+
lines.push(`# Detail: ${jql}`);
|
|
382
|
+
lines.push(`Analyzed ${allIssues.length} issues (as of ${formatDateShort(now)})`);
|
|
383
|
+
if (truncated) {
|
|
384
|
+
lines.push(`*Results capped at ${maxResults} issues — query may match more.*`);
|
|
385
|
+
}
|
|
386
|
+
// Render requested metrics
|
|
387
|
+
const renderers = {
|
|
388
|
+
points: () => renderPoints(allIssues),
|
|
389
|
+
time: () => renderTime(allIssues),
|
|
390
|
+
schedule: () => renderSchedule(allIssues, now),
|
|
391
|
+
cycle: () => renderCycle(allIssues, now),
|
|
392
|
+
distribution: () => renderDistribution(allIssues),
|
|
393
|
+
};
|
|
394
|
+
for (const metric of fetchMetrics) {
|
|
395
|
+
lines.push('');
|
|
396
|
+
lines.push(renderers[metric]());
|
|
397
|
+
}
|
|
290
398
|
}
|
|
291
399
|
// Next steps
|
|
292
400
|
const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const toolSchemas = {
|
|
2
2
|
manage_jira_filter: {
|
|
3
3
|
name: 'manage_jira_filter',
|
|
4
|
-
description: 'Search for issues using JQL queries, or manage saved filters',
|
|
4
|
+
description: 'Search for issues using JQL queries, or manage saved filters. Returns issue details (title, status, description). For quantitative questions (counts, totals, overdue, workload), use analyze_jira_issues instead.',
|
|
5
5
|
inputSchema: {
|
|
6
6
|
type: 'object',
|
|
7
7
|
properties: {
|
|
@@ -242,7 +242,7 @@ export const toolSchemas = {
|
|
|
242
242
|
},
|
|
243
243
|
manage_jira_project: {
|
|
244
244
|
name: 'manage_jira_project',
|
|
245
|
-
description: 'List projects or get project
|
|
245
|
+
description: 'List projects or get project configuration and metadata. For issue counts, workload, or cross-project comparison, use analyze_jira_issues with metrics: ["summary"] instead.',
|
|
246
246
|
inputSchema: {
|
|
247
247
|
type: 'object',
|
|
248
248
|
properties: {
|
|
@@ -326,25 +326,30 @@ export const toolSchemas = {
|
|
|
326
326
|
},
|
|
327
327
|
analyze_jira_issues: {
|
|
328
328
|
name: 'analyze_jira_issues',
|
|
329
|
-
description: 'Compute project metrics over
|
|
329
|
+
description: 'Compute project metrics over issues selected by JQL. Use "summary" for exact counts across projects (no sampling cap). Use other metrics for detailed analysis of individual issue data. Always prefer this tool over manage_jira_filter or manage_jira_project for quantitative questions (counts, totals, overdue, workload).',
|
|
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: "
|
|
335
|
+
description: 'JQL query selecting the issues to analyze. Examples: "project in (AA, GC, LGS)", "sprint in openSprints()", "assignee = currentUser() AND resolution = Unresolved".',
|
|
336
336
|
},
|
|
337
337
|
metrics: {
|
|
338
338
|
type: 'array',
|
|
339
339
|
items: {
|
|
340
340
|
type: 'string',
|
|
341
|
-
enum: ['points', 'time', 'schedule', 'cycle', 'distribution'],
|
|
341
|
+
enum: ['summary', 'points', 'time', 'schedule', 'cycle', 'distribution'],
|
|
342
342
|
},
|
|
343
|
-
description: 'Which metric groups to
|
|
343
|
+
description: 'Which metric groups to compute. summary = exact issue counts via count API (no cap, fastest). points = earned value/SPI. time = effort estimates. schedule = overdue/risk. cycle = lead time/throughput. distribution = counts by status/assignee/priority/type. Default: all except summary.',
|
|
344
|
+
},
|
|
345
|
+
groupBy: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
enum: ['project', 'assignee', 'priority', 'issuetype'],
|
|
348
|
+
description: 'Split summary counts by this dimension. Use with metrics: ["summary"]. "project" produces a per-project comparison table.',
|
|
344
349
|
},
|
|
345
350
|
maxResults: {
|
|
346
351
|
type: 'integer',
|
|
347
|
-
description: 'Max issues to
|
|
352
|
+
description: 'Max issues to fetch for detail metrics (default 100, max 500). Does not apply to summary (which uses count API).',
|
|
348
353
|
default: 100,
|
|
349
354
|
},
|
|
350
355
|
},
|
package/package.json
CHANGED