@aaronsb/jira-cloud-mcp 0.7.2 → 0.8.0
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 +26 -1
- package/build/docs/tool-documentation.js +28 -10
- package/build/handlers/analysis-handler.js +20 -25
- package/build/handlers/issue-handlers.js +48 -3
- package/build/mcp/markdown-renderer.js +26 -10
- package/build/schemas/tool-schemas.js +35 -2
- package/build/utils/next-steps.js +3 -0
- package/package.json +1 -1
- package/build/handlers/search-handlers.js +0 -103
- package/build/schemas/request-schemas.js +0 -187
- package/build/utils/formatters/base-formatter.js +0 -58
- package/build/utils/formatters/board-formatter.js +0 -63
- package/build/utils/formatters/filter-formatter.js +0 -66
- package/build/utils/formatters/index.js +0 -7
- package/build/utils/formatters/issue-formatter.js +0 -84
- package/build/utils/formatters/project-formatter.js +0 -55
- package/build/utils/formatters/search-formatter.js +0 -62
- package/build/utils/formatters/sprint-formatter.js +0 -111
|
@@ -94,6 +94,8 @@ export class JiraClient {
|
|
|
94
94
|
this.customFields.startDate,
|
|
95
95
|
this.customFields.storyPoints,
|
|
96
96
|
'timeestimate',
|
|
97
|
+
'timeoriginalestimate',
|
|
98
|
+
'timespent',
|
|
97
99
|
...(this.customFields.sprint ? [this.customFields.sprint] : []),
|
|
98
100
|
'issuelinks',
|
|
99
101
|
];
|
|
@@ -147,6 +149,8 @@ export class JiraClient {
|
|
|
147
149
|
startDate: fields?.[this.customFields.startDate] || null,
|
|
148
150
|
storyPoints: fields?.[this.customFields.storyPoints] ?? null,
|
|
149
151
|
timeEstimate: fields?.timeestimate ?? null,
|
|
152
|
+
originalEstimate: fields?.timeoriginalestimate ?? null,
|
|
153
|
+
timeSpent: fields?.timespent ?? null,
|
|
150
154
|
sprint: this.customFields.sprint ? this.extractSprintName(fields?.[this.customFields.sprint]) : null,
|
|
151
155
|
issueLinks: (fields?.issuelinks || []).map((link) => ({
|
|
152
156
|
type: link.type?.name || '',
|
|
@@ -502,6 +506,12 @@ export class JiraClient {
|
|
|
502
506
|
fields.labels = params.labels;
|
|
503
507
|
if (params.dueDate !== undefined)
|
|
504
508
|
fields.duedate = params.dueDate;
|
|
509
|
+
if (params.originalEstimate || params.remainingEstimate) {
|
|
510
|
+
fields.timetracking = {
|
|
511
|
+
...(params.originalEstimate ? { originalEstimate: params.originalEstimate } : {}),
|
|
512
|
+
...(params.remainingEstimate ? { remainingEstimate: params.remainingEstimate } : {}),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
505
515
|
if (params.customFields) {
|
|
506
516
|
Object.assign(fields, this.convertAdfFields(params.customFields));
|
|
507
517
|
}
|
|
@@ -510,6 +520,17 @@ export class JiraClient {
|
|
|
510
520
|
fields,
|
|
511
521
|
});
|
|
512
522
|
}
|
|
523
|
+
async addWorklog(params) {
|
|
524
|
+
await this.client.issueWorklogs.addWorklog({
|
|
525
|
+
issueIdOrKey: params.issueKey,
|
|
526
|
+
timeSpent: params.timeSpent,
|
|
527
|
+
comment: params.comment,
|
|
528
|
+
started: params.started,
|
|
529
|
+
adjustEstimate: params.adjustEstimate ?? 'auto',
|
|
530
|
+
newEstimate: params.newEstimate,
|
|
531
|
+
reduceBy: params.reduceBy,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
513
534
|
async addComment(issueKey, commentBody) {
|
|
514
535
|
await this.client.issueComments.addComment({
|
|
515
536
|
issueIdOrKey: issueKey,
|
|
@@ -536,7 +557,8 @@ export class JiraClient {
|
|
|
536
557
|
const leanFields = [
|
|
537
558
|
'summary', 'issuetype', 'priority', 'assignee', 'reporter',
|
|
538
559
|
'status', 'resolution', 'labels', 'created', 'updated',
|
|
539
|
-
'resolutiondate', 'statuscategorychangedate', 'duedate',
|
|
560
|
+
'resolutiondate', 'statuscategorychangedate', 'duedate',
|
|
561
|
+
'timeestimate', 'timeoriginalestimate', 'timespent',
|
|
540
562
|
this.customFields.startDate, this.customFields.storyPoints,
|
|
541
563
|
...(this.customFields.sprint ? [this.customFields.sprint] : []),
|
|
542
564
|
];
|
|
@@ -1057,6 +1079,9 @@ export class JiraClient {
|
|
|
1057
1079
|
fields.labels = params.labels;
|
|
1058
1080
|
if (params.dueDate)
|
|
1059
1081
|
fields.duedate = params.dueDate;
|
|
1082
|
+
if (params.originalEstimate) {
|
|
1083
|
+
fields.timetracking = { originalEstimate: params.originalEstimate };
|
|
1084
|
+
}
|
|
1060
1085
|
if (params.customFields) {
|
|
1061
1086
|
Object.assign(fields, this.convertAdfFields(params.customFields));
|
|
1062
1087
|
}
|
|
@@ -68,18 +68,34 @@ function generateIssueToolDocumentation(schema) {
|
|
|
68
68
|
update: {
|
|
69
69
|
description: "Updates an existing issue",
|
|
70
70
|
required_parameters: ["issueKey"],
|
|
71
|
-
optional_parameters: ["summary", "description", "assignee", "priority", "labels", "customFields"],
|
|
71
|
+
optional_parameters: ["summary", "description", "assignee", "priority", "labels", "originalEstimate", "remainingEstimate", "customFields"],
|
|
72
72
|
examples: [
|
|
73
73
|
{
|
|
74
74
|
description: "Update issue summary",
|
|
75
75
|
code: { operation: "update", issueKey: "PROJ-123", summary: "Updated feature request" }
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
|
-
description: "
|
|
79
|
-
code: {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
description: "Set time estimate on an issue",
|
|
79
|
+
code: { operation: "update", issueKey: "PROJ-123", originalEstimate: "3d", remainingEstimate: "2d" }
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
worklog: {
|
|
84
|
+
description: "Logs time spent on an issue",
|
|
85
|
+
required_parameters: ["issueKey", "timeSpent"],
|
|
86
|
+
optional_parameters: ["worklogComment", "started", "adjustEstimate", "newEstimate", "reduceBy"],
|
|
87
|
+
examples: [
|
|
88
|
+
{
|
|
89
|
+
description: "Log 3.5 hours of work",
|
|
90
|
+
code: { operation: "worklog", issueKey: "PROJ-123", timeSpent: "3h 30m", worklogComment: "Implemented feature X" }
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
description: "Log work with specific start time",
|
|
94
|
+
code: { operation: "worklog", issueKey: "PROJ-123", timeSpent: "1d", started: "2025-04-09T09:00:00.000+0000", worklogComment: "Design review" }
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
description: "Log work without adjusting estimate",
|
|
98
|
+
code: { operation: "worklog", issueKey: "PROJ-123", timeSpent: "2h", adjustEstimate: "leave" }
|
|
83
99
|
}
|
|
84
100
|
]
|
|
85
101
|
},
|
|
@@ -122,11 +138,13 @@ function generateIssueToolDocumentation(schema) {
|
|
|
122
138
|
},
|
|
123
139
|
common_use_cases: [
|
|
124
140
|
{
|
|
125
|
-
title: "
|
|
126
|
-
description: "To
|
|
141
|
+
title: "Time tracking",
|
|
142
|
+
description: "To estimate and log time on issues:",
|
|
127
143
|
steps: [
|
|
128
|
-
{ description: "
|
|
129
|
-
{ description: "
|
|
144
|
+
{ description: "Set an estimate when creating", code: { operation: "create", projectKey: "PROJ", summary: "New task", issueType: "Task", originalEstimate: "3d" } },
|
|
145
|
+
{ description: "Update the estimate", code: { operation: "update", issueKey: "PROJ-123", originalEstimate: "5d", remainingEstimate: "3d" } },
|
|
146
|
+
{ description: "Log time spent", code: { operation: "worklog", issueKey: "PROJ-123", timeSpent: "3h 30m", worklogComment: "Implemented feature X" } },
|
|
147
|
+
{ description: "View time tracking on an issue", code: { operation: "get", issueKey: "PROJ-123" } }
|
|
130
148
|
]
|
|
131
149
|
},
|
|
132
150
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { GraphQLHierarchyWalker, walkTree } from '../client/graphql-hierarchy.js';
|
|
3
3
|
import { renderRollupTree } from '../handlers/plan-handler.js';
|
|
4
|
+
import { formatDuration } from '../mcp/markdown-renderer.js';
|
|
4
5
|
import { evaluateRow, extractColumnRefs, parseComputeList } from '../utils/cube-dsl.js';
|
|
5
6
|
import { analysisNextSteps } from '../utils/next-steps.js';
|
|
6
7
|
import { normalizeArgs } from '../utils/normalize-args.js';
|
|
@@ -39,18 +40,6 @@ function formatDateShort(date) {
|
|
|
39
40
|
function daysBetween(a, b) {
|
|
40
41
|
return Math.round((b.getTime() - a.getTime()) / (1000 * 60 * 60 * 24));
|
|
41
42
|
}
|
|
42
|
-
function formatDuration(seconds) {
|
|
43
|
-
const hours = Math.floor(seconds / 3600);
|
|
44
|
-
const days = Math.floor(hours / 8); // 8-hour work day
|
|
45
|
-
if (days > 0) {
|
|
46
|
-
const remainingHours = hours % 8;
|
|
47
|
-
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
|
|
48
|
-
}
|
|
49
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
50
|
-
if (hours > 0)
|
|
51
|
-
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
52
|
-
return `${minutes}m`;
|
|
53
|
-
}
|
|
54
43
|
function countBy(items, keyFn) {
|
|
55
44
|
const counts = new Map();
|
|
56
45
|
for (const item of items) {
|
|
@@ -114,22 +103,26 @@ export function renderPoints(issues) {
|
|
|
114
103
|
return lines.join('\n');
|
|
115
104
|
}
|
|
116
105
|
export function renderTime(issues) {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
const done = byBucket.get('Done') ?? 0;
|
|
126
|
-
const remaining = total - done;
|
|
106
|
+
const hasOriginal = issues.some(i => i.originalEstimate != null);
|
|
107
|
+
const hasLogged = issues.some(i => i.timeSpent != null);
|
|
108
|
+
const hasRemaining = issues.some(i => i.timeEstimate != null);
|
|
109
|
+
const originalTotal = sumBy(issues, i => i.originalEstimate);
|
|
110
|
+
const loggedTotal = sumBy(issues, i => i.timeSpent);
|
|
111
|
+
const remainingTotal = sumBy(issues, i => i.timeEstimate);
|
|
112
|
+
const unestimated = issues.filter(i => i.originalEstimate == null && i.timeEstimate == null).length;
|
|
127
113
|
const lines = ['## Time (Effort)', ''];
|
|
128
114
|
lines.push('| Metric | Value |');
|
|
129
115
|
lines.push('|--------|-------|');
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
116
|
+
if (hasOriginal)
|
|
117
|
+
lines.push(`| Original Estimate | ${formatDuration(originalTotal)} |`);
|
|
118
|
+
if (hasLogged)
|
|
119
|
+
lines.push(`| Logged | ${formatDuration(loggedTotal)} |`);
|
|
120
|
+
if (hasRemaining)
|
|
121
|
+
lines.push(`| Remaining | ${formatDuration(remainingTotal)} |`);
|
|
122
|
+
if (hasOriginal && hasLogged && originalTotal > 0) {
|
|
123
|
+
const pct = Math.round((loggedTotal / originalTotal) * 100);
|
|
124
|
+
lines.push(`| Effort Used | ${pct}% |`);
|
|
125
|
+
}
|
|
133
126
|
if (unestimated > 0) {
|
|
134
127
|
lines.push(`| Unestimated | ${unestimated} issue${unestimated !== 1 ? 's' : ''} |`);
|
|
135
128
|
}
|
|
@@ -779,6 +772,8 @@ function graphIssueToDetails(issue) {
|
|
|
779
772
|
storyPoints: issue.storyPoints,
|
|
780
773
|
sprint: null,
|
|
781
774
|
timeEstimate: null,
|
|
775
|
+
originalEstimate: null,
|
|
776
|
+
timeSpent: null,
|
|
782
777
|
issueLinks: [],
|
|
783
778
|
};
|
|
784
779
|
}
|
|
@@ -13,8 +13,8 @@ function validateManageJiraIssueArgs(args) {
|
|
|
13
13
|
const normalizedArgs = normalizeArgs(args);
|
|
14
14
|
// Validate operation parameter
|
|
15
15
|
if (typeof normalizedArgs.operation !== 'string' ||
|
|
16
|
-
!['create', 'get', 'update', 'delete', 'move', 'transition', 'comment', 'link', 'hierarchy'].includes(normalizedArgs.operation)) {
|
|
17
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid operation parameter. Valid values are: create, get, update, delete, move, transition, comment, link, hierarchy');
|
|
16
|
+
!['create', 'get', 'update', 'delete', 'move', 'transition', 'comment', 'link', 'hierarchy', 'worklog'].includes(normalizedArgs.operation)) {
|
|
17
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid operation parameter. Valid values are: create, get, update, delete, move, transition, comment, link, hierarchy, worklog');
|
|
18
18
|
}
|
|
19
19
|
// Validate parameters based on operation
|
|
20
20
|
switch (normalizedArgs.operation) {
|
|
@@ -62,8 +62,10 @@ function validateManageJiraIssueArgs(args) {
|
|
|
62
62
|
normalizedArgs.priority === undefined &&
|
|
63
63
|
normalizedArgs.labels === undefined &&
|
|
64
64
|
normalizedArgs.dueDate === undefined &&
|
|
65
|
+
normalizedArgs.originalEstimate === undefined &&
|
|
66
|
+
normalizedArgs.remainingEstimate === undefined &&
|
|
65
67
|
normalizedArgs.customFields === undefined) {
|
|
66
|
-
throw new McpError(ErrorCode.InvalidParams, 'At least one update field (summary, description, parent, assignee, priority, labels, dueDate, or customFields) must be provided for the update operation.');
|
|
68
|
+
throw new McpError(ErrorCode.InvalidParams, 'At least one update field (summary, description, parent, assignee, priority, labels, dueDate, originalEstimate, remainingEstimate, or customFields) must be provided for the update operation.');
|
|
67
69
|
}
|
|
68
70
|
break;
|
|
69
71
|
case 'transition':
|
|
@@ -93,6 +95,20 @@ function validateManageJiraIssueArgs(args) {
|
|
|
93
95
|
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid linkType parameter. Please provide a valid link type for the link operation.');
|
|
94
96
|
}
|
|
95
97
|
break;
|
|
98
|
+
case 'worklog':
|
|
99
|
+
if (typeof normalizedArgs.issueKey !== 'string' || normalizedArgs.issueKey.trim() === '') {
|
|
100
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid issueKey parameter. Please provide a valid issue key for the worklog operation.');
|
|
101
|
+
}
|
|
102
|
+
if (typeof normalizedArgs.timeSpent !== 'string' || normalizedArgs.timeSpent.trim() === '') {
|
|
103
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid timeSpent parameter. Provide time in Jira format (e.g., "3h 30m", "1d", "2w").');
|
|
104
|
+
}
|
|
105
|
+
if (normalizedArgs.adjustEstimate === 'new' && !normalizedArgs.newEstimate) {
|
|
106
|
+
throw new McpError(ErrorCode.InvalidParams, 'newEstimate is required when adjustEstimate is "new" (e.g., "2d").');
|
|
107
|
+
}
|
|
108
|
+
if (normalizedArgs.adjustEstimate === 'manual' && !normalizedArgs.reduceBy) {
|
|
109
|
+
throw new McpError(ErrorCode.InvalidParams, 'reduceBy is required when adjustEstimate is "manual" (e.g., "1h").');
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
96
112
|
case 'hierarchy':
|
|
97
113
|
if (typeof normalizedArgs.issueKey !== 'string' || normalizedArgs.issueKey.trim() === '') {
|
|
98
114
|
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid issueKey parameter. Please provide a valid issue key for the hierarchy operation.');
|
|
@@ -258,6 +274,7 @@ async function handleCreateIssue(jiraClient, args) {
|
|
|
258
274
|
assignee: args.assignee,
|
|
259
275
|
labels: args.labels,
|
|
260
276
|
dueDate: args.dueDate ?? undefined,
|
|
277
|
+
originalEstimate: args.originalEstimate,
|
|
261
278
|
customFields,
|
|
262
279
|
});
|
|
263
280
|
// Get the created issue and render to markdown
|
|
@@ -283,6 +300,8 @@ async function handleUpdateIssue(jiraClient, args) {
|
|
|
283
300
|
priority: args.priority,
|
|
284
301
|
labels: args.labels,
|
|
285
302
|
dueDate: args.dueDate,
|
|
303
|
+
originalEstimate: args.originalEstimate,
|
|
304
|
+
remainingEstimate: args.remainingEstimate,
|
|
286
305
|
customFields,
|
|
287
306
|
});
|
|
288
307
|
// Get the updated issue and render to markdown
|
|
@@ -341,6 +360,28 @@ async function handleLinkIssue(jiraClient, args) {
|
|
|
341
360
|
],
|
|
342
361
|
};
|
|
343
362
|
}
|
|
363
|
+
async function handleWorklogIssue(jiraClient, args) {
|
|
364
|
+
await jiraClient.addWorklog({
|
|
365
|
+
issueKey: args.issueKey,
|
|
366
|
+
timeSpent: args.timeSpent,
|
|
367
|
+
comment: args.worklogComment,
|
|
368
|
+
started: args.started,
|
|
369
|
+
adjustEstimate: args.adjustEstimate,
|
|
370
|
+
newEstimate: args.newEstimate,
|
|
371
|
+
reduceBy: args.reduceBy,
|
|
372
|
+
});
|
|
373
|
+
// Get the updated issue and render to markdown
|
|
374
|
+
const updatedIssue = await jiraClient.getIssue(args.issueKey, false, false);
|
|
375
|
+
const markdown = MarkdownRenderer.renderIssue(updatedIssue);
|
|
376
|
+
return {
|
|
377
|
+
content: [
|
|
378
|
+
{
|
|
379
|
+
type: 'text',
|
|
380
|
+
text: `# Worklog Added\n\nLogged ${args.timeSpent} on ${args.issueKey}\n\n${markdown}${issueGuidance('worklog', args.issueKey)}`,
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
344
385
|
function renderHierarchyTree(node, focusKey, prefix = '', isLast = true, isRoot = true) {
|
|
345
386
|
const connector = isRoot ? '' : (isLast ? '└─ ' : '├─ ');
|
|
346
387
|
const marker = node.key === focusKey ? ' ← you are here' : '';
|
|
@@ -419,6 +460,10 @@ export async function handleIssueRequest(jiraClient, request) {
|
|
|
419
460
|
console.error('Processing link issue operation');
|
|
420
461
|
return await handleLinkIssue(jiraClient, normalizedArgs);
|
|
421
462
|
}
|
|
463
|
+
case 'worklog': {
|
|
464
|
+
console.error('Processing worklog operation');
|
|
465
|
+
return await handleWorklogIssue(jiraClient, normalizedArgs);
|
|
466
|
+
}
|
|
422
467
|
case 'hierarchy': {
|
|
423
468
|
console.error('Processing hierarchy operation');
|
|
424
469
|
return await handleHierarchy(jiraClient, normalizedArgs);
|
|
@@ -32,6 +32,22 @@ function formatDate(dateStr) {
|
|
|
32
32
|
const date = new Date(dateStr);
|
|
33
33
|
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
34
34
|
}
|
|
35
|
+
/** Format seconds into human-readable duration (e.g. 1d 2h, 3h 30m) */
|
|
36
|
+
export function formatDuration(seconds) {
|
|
37
|
+
if (seconds === 0)
|
|
38
|
+
return '0m';
|
|
39
|
+
const days = Math.floor(seconds / 28800); // 8h workday
|
|
40
|
+
const hours = Math.floor((seconds % 28800) / 3600);
|
|
41
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
42
|
+
const parts = [];
|
|
43
|
+
if (days > 0)
|
|
44
|
+
parts.push(`${days}d`);
|
|
45
|
+
if (hours > 0)
|
|
46
|
+
parts.push(`${hours}h`);
|
|
47
|
+
if (minutes > 0)
|
|
48
|
+
parts.push(`${minutes}m`);
|
|
49
|
+
return parts.length > 0 ? parts.join(' ') : '0m';
|
|
50
|
+
}
|
|
35
51
|
/**
|
|
36
52
|
* Format status with visual indicator
|
|
37
53
|
*/
|
|
@@ -110,16 +126,16 @@ export function renderIssue(issue, transitions) {
|
|
|
110
126
|
lines.push(dates.join(' | '));
|
|
111
127
|
if (issue.storyPoints)
|
|
112
128
|
lines.push(`Points: ${issue.storyPoints}`);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
// Time tracking — consolidated line
|
|
130
|
+
const timeParts = [];
|
|
131
|
+
if (issue.originalEstimate != null)
|
|
132
|
+
timeParts.push(`Estimate: ${formatDuration(issue.originalEstimate)}`);
|
|
133
|
+
if (issue.timeEstimate != null)
|
|
134
|
+
timeParts.push(`Remaining: ${formatDuration(issue.timeEstimate)}`);
|
|
135
|
+
if (issue.timeSpent != null)
|
|
136
|
+
timeParts.push(`Logged: ${formatDuration(issue.timeSpent)}`);
|
|
137
|
+
if (timeParts.length > 0)
|
|
138
|
+
lines.push(timeParts.join(' | '));
|
|
123
139
|
if (issue.resolution)
|
|
124
140
|
lines.push(`Resolution: ${issue.resolution}`);
|
|
125
141
|
// Description — already markdown from ADF conversion
|
|
@@ -142,13 +142,13 @@ export const toolSchemas = {
|
|
|
142
142
|
},
|
|
143
143
|
manage_jira_issue: {
|
|
144
144
|
name: 'manage_jira_issue',
|
|
145
|
-
description: 'Get, create, update, delete, move, transition, comment on, link, or explore hierarchy of Jira issues',
|
|
145
|
+
description: 'Get, create, update, delete, move, transition, comment on, link, log work on, or explore hierarchy of Jira issues',
|
|
146
146
|
inputSchema: {
|
|
147
147
|
type: 'object',
|
|
148
148
|
properties: {
|
|
149
149
|
operation: {
|
|
150
150
|
type: 'string',
|
|
151
|
-
enum: ['create', 'get', 'update', 'delete', 'move', 'transition', 'comment', 'link', 'hierarchy'],
|
|
151
|
+
enum: ['create', 'get', 'update', 'delete', 'move', 'transition', 'comment', 'link', 'hierarchy', 'worklog'],
|
|
152
152
|
description: 'Operation to perform',
|
|
153
153
|
},
|
|
154
154
|
issueKey: {
|
|
@@ -192,6 +192,39 @@ export const toolSchemas = {
|
|
|
192
192
|
type: ['string', 'null'],
|
|
193
193
|
description: 'Due date in ISO format (e.g., "2025-06-15") or null to clear. For create and update.',
|
|
194
194
|
},
|
|
195
|
+
originalEstimate: {
|
|
196
|
+
type: 'string',
|
|
197
|
+
description: 'Original time estimate in Jira format (e.g., "3d", "2h 30m", "1w"). For create and update.',
|
|
198
|
+
},
|
|
199
|
+
remainingEstimate: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
description: 'Remaining time estimate in Jira format (e.g., "1d", "4h"). For update only.',
|
|
202
|
+
},
|
|
203
|
+
timeSpent: {
|
|
204
|
+
type: 'string',
|
|
205
|
+
description: 'Time spent in Jira format (e.g., "3h 30m", "1d", "2w"). Required for worklog.',
|
|
206
|
+
},
|
|
207
|
+
worklogComment: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
description: 'Description of work performed. For worklog.',
|
|
210
|
+
},
|
|
211
|
+
started: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
description: 'When the work started, ISO datetime (e.g., "2025-04-09T09:00:00.000+0000"). Defaults to now. For worklog.',
|
|
214
|
+
},
|
|
215
|
+
adjustEstimate: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
enum: ['auto', 'leave', 'new', 'manual'],
|
|
218
|
+
description: 'How to adjust the remaining estimate: auto (reduce by timeSpent), leave (unchanged), new (set to newEstimate), manual (reduce by reduceBy). Default: auto. For worklog.',
|
|
219
|
+
},
|
|
220
|
+
newEstimate: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: 'New remaining estimate when adjustEstimate is "new" (e.g., "2d"). For worklog.',
|
|
223
|
+
},
|
|
224
|
+
reduceBy: {
|
|
225
|
+
type: 'string',
|
|
226
|
+
description: 'Amount to reduce remaining estimate when adjustEstimate is "manual" (e.g., "1h"). For worklog.',
|
|
227
|
+
},
|
|
195
228
|
parent: {
|
|
196
229
|
type: ['string', 'null'],
|
|
197
230
|
description: 'Parent issue key (e.g., PROJ-100) or null to remove.',
|
|
@@ -38,6 +38,9 @@ export function issueNextSteps(operation, issueKey) {
|
|
|
38
38
|
case 'link':
|
|
39
39
|
steps.push({ description: 'View the linked issue', tool: 'manage_jira_issue', example: { operation: 'get', issueKey } }, { description: 'Read available link types from jira://issue-link-types resource' });
|
|
40
40
|
break;
|
|
41
|
+
case 'worklog':
|
|
42
|
+
steps.push({ description: 'View the updated issue', tool: 'manage_jira_issue', example: { operation: 'get', issueKey } }, { description: 'Log more time', tool: 'manage_jira_issue', example: { operation: 'worklog', issueKey, timeSpent: '<duration>' } }, { description: 'Adjust the remaining estimate', tool: 'manage_jira_issue', example: { operation: 'update', issueKey, remainingEstimate: '<duration>' } });
|
|
43
|
+
break;
|
|
41
44
|
case 'hierarchy':
|
|
42
45
|
steps.push({ description: 'View a specific issue from the tree', tool: 'manage_jira_issue', example: { operation: 'get', issueKey } }, { description: 'Analyze plan rollups (requires Jira Plans)', tool: 'analyze_jira_plan', example: { issueKey } }, { description: 'Search for issues in this project', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql: `project = "${issueKey?.split('-')[0]}"` } });
|
|
43
46
|
break;
|
package/package.json
CHANGED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { MarkdownRenderer } from '../mcp/markdown-renderer.js';
|
|
3
|
-
// Helper function to normalize parameter names (support both snake_case and camelCase)
|
|
4
|
-
function normalizeArgs(args) {
|
|
5
|
-
const normalized = {};
|
|
6
|
-
for (const [key, value] of Object.entries(args)) {
|
|
7
|
-
// Convert snake_case to camelCase
|
|
8
|
-
if (key === 'start_at') {
|
|
9
|
-
normalized['startAt'] = value;
|
|
10
|
-
}
|
|
11
|
-
else if (key === 'max_results') {
|
|
12
|
-
normalized['maxResults'] = value;
|
|
13
|
-
}
|
|
14
|
-
else if (key === 'filter_id') {
|
|
15
|
-
normalized['filterId'] = value;
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
normalized[key] = value;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return normalized;
|
|
22
|
-
}
|
|
23
|
-
function isSearchIssuesArgs(args) {
|
|
24
|
-
if (typeof args !== 'object' || args === null) {
|
|
25
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid search_jira_issues arguments: Expected an object with a jql parameter. Example: { "jql": "project = PROJ" }`);
|
|
26
|
-
}
|
|
27
|
-
const typedArgs = args;
|
|
28
|
-
if (typeof typedArgs.jql !== 'string') {
|
|
29
|
-
throw new McpError(ErrorCode.InvalidParams, `Missing or invalid jql parameter. Please provide a valid JQL query string. Example: { "jql": "project = PROJ" }`);
|
|
30
|
-
}
|
|
31
|
-
// Validate startAt if present
|
|
32
|
-
if (typedArgs.startAt !== undefined && typeof typedArgs.startAt !== 'number') {
|
|
33
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid startAt parameter. Expected a number.');
|
|
34
|
-
}
|
|
35
|
-
// Validate maxResults if present
|
|
36
|
-
if (typedArgs.maxResults !== undefined && typeof typedArgs.maxResults !== 'number') {
|
|
37
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid maxResults parameter. Expected a number.');
|
|
38
|
-
}
|
|
39
|
-
// Validate expand parameter if present
|
|
40
|
-
if (typedArgs.expand !== undefined) {
|
|
41
|
-
if (!Array.isArray(typedArgs.expand)) {
|
|
42
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid expand parameter. Expected an array of strings.');
|
|
43
|
-
}
|
|
44
|
-
const validExpansions = ['issue_details', 'transitions', 'comments_preview'];
|
|
45
|
-
for (const expansion of typedArgs.expand) {
|
|
46
|
-
if (typeof expansion !== 'string' || !validExpansions.includes(expansion)) {
|
|
47
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid expansion: ${expansion}. Valid expansions are: ${validExpansions.join(', ')}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
export async function setupSearchHandlers(server, jiraClient, request) {
|
|
54
|
-
console.error('Handling search request...');
|
|
55
|
-
const { name } = request.params;
|
|
56
|
-
const args = request.params.arguments;
|
|
57
|
-
if (!args) {
|
|
58
|
-
throw new McpError(ErrorCode.InvalidParams, 'Missing arguments');
|
|
59
|
-
}
|
|
60
|
-
// Normalize arguments to support both snake_case and camelCase
|
|
61
|
-
const normalizedArgs = normalizeArgs(args);
|
|
62
|
-
switch (name) {
|
|
63
|
-
case 'search_jira_issues': {
|
|
64
|
-
console.error('Processing search_jira_issues request');
|
|
65
|
-
if (!isSearchIssuesArgs(normalizedArgs)) {
|
|
66
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid search_jira_issues arguments');
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
console.error(`Executing search with args:`, JSON.stringify(normalizedArgs, null, 2));
|
|
70
|
-
// Parse expansion options (reserved for future use)
|
|
71
|
-
const _expansionOptions = {};
|
|
72
|
-
if (normalizedArgs.expand) {
|
|
73
|
-
for (const expansion of normalizedArgs.expand) {
|
|
74
|
-
_expansionOptions[expansion] = true;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// Execute the search
|
|
78
|
-
const searchResult = await jiraClient.searchIssues(normalizedArgs.jql, normalizedArgs.startAt, normalizedArgs.maxResults);
|
|
79
|
-
// Render to markdown for token efficiency
|
|
80
|
-
const markdown = MarkdownRenderer.renderIssueSearchResults(searchResult.issues, searchResult.pagination, normalizedArgs.jql);
|
|
81
|
-
return {
|
|
82
|
-
content: [
|
|
83
|
-
{
|
|
84
|
-
type: 'text',
|
|
85
|
-
text: markdown,
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
console.error('Error in search_jira_issues:', error);
|
|
92
|
-
if (error instanceof Error) {
|
|
93
|
-
throw new McpError(ErrorCode.InvalidRequest, `Jira API error: ${error.message}`);
|
|
94
|
-
}
|
|
95
|
-
throw new McpError(ErrorCode.InvalidRequest, 'Failed to execute Jira search');
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
default: {
|
|
99
|
-
console.error(`Unknown tool requested: ${name}`);
|
|
100
|
-
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
// Sprint Management Schemas
|
|
3
|
-
export const CreateJiraSprintSchema = z.object({
|
|
4
|
-
method: z.literal('tools/call'),
|
|
5
|
-
params: z.object({
|
|
6
|
-
name: z.literal('create_jira_sprint'),
|
|
7
|
-
arguments: z.object({
|
|
8
|
-
boardId: z.number(),
|
|
9
|
-
name: z.string(),
|
|
10
|
-
startDate: z.string().optional(),
|
|
11
|
-
endDate: z.string().optional(),
|
|
12
|
-
goal: z.string().optional(),
|
|
13
|
-
}),
|
|
14
|
-
}),
|
|
15
|
-
});
|
|
16
|
-
export const GetJiraSprintSchema = z.object({
|
|
17
|
-
method: z.literal('tools/call'),
|
|
18
|
-
params: z.object({
|
|
19
|
-
name: z.literal('get_jira_sprint'),
|
|
20
|
-
arguments: z.object({
|
|
21
|
-
sprintId: z.number(),
|
|
22
|
-
expand: z.array(z.string()).optional(),
|
|
23
|
-
}),
|
|
24
|
-
}),
|
|
25
|
-
});
|
|
26
|
-
export const ListJiraSprintsSchema = z.object({
|
|
27
|
-
method: z.literal('tools/call'),
|
|
28
|
-
params: z.object({
|
|
29
|
-
name: z.literal('list_jira_sprints'),
|
|
30
|
-
arguments: z.object({
|
|
31
|
-
boardId: z.number(),
|
|
32
|
-
state: z.enum(['future', 'active', 'closed']).optional(),
|
|
33
|
-
startAt: z.number().optional(),
|
|
34
|
-
maxResults: z.number().optional(),
|
|
35
|
-
expand: z.array(z.string()).optional(),
|
|
36
|
-
}),
|
|
37
|
-
}),
|
|
38
|
-
});
|
|
39
|
-
export const UpdateJiraSprintSchema = z.object({
|
|
40
|
-
method: z.literal('tools/call'),
|
|
41
|
-
params: z.object({
|
|
42
|
-
name: z.literal('update_jira_sprint'),
|
|
43
|
-
arguments: z.object({
|
|
44
|
-
sprintId: z.number(),
|
|
45
|
-
name: z.string().optional(),
|
|
46
|
-
goal: z.string().optional(),
|
|
47
|
-
startDate: z.string().optional(),
|
|
48
|
-
endDate: z.string().optional(),
|
|
49
|
-
state: z.enum(['future', 'active', 'closed']).optional(),
|
|
50
|
-
}),
|
|
51
|
-
}),
|
|
52
|
-
});
|
|
53
|
-
export const DeleteJiraSprintSchema = z.object({
|
|
54
|
-
method: z.literal('tools/call'),
|
|
55
|
-
params: z.object({
|
|
56
|
-
name: z.literal('delete_jira_sprint'),
|
|
57
|
-
arguments: z.object({
|
|
58
|
-
sprintId: z.number(),
|
|
59
|
-
}),
|
|
60
|
-
}),
|
|
61
|
-
});
|
|
62
|
-
export const UpdateSprintIssuesSchema = z.object({
|
|
63
|
-
method: z.literal('tools/call'),
|
|
64
|
-
params: z.object({
|
|
65
|
-
name: z.literal('update_sprint_issues'),
|
|
66
|
-
arguments: z.object({
|
|
67
|
-
sprintId: z.number(),
|
|
68
|
-
add: z.array(z.string()).optional(),
|
|
69
|
-
remove: z.array(z.string()).optional(),
|
|
70
|
-
}),
|
|
71
|
-
}),
|
|
72
|
-
});
|
|
73
|
-
// Consolidated API request schemas
|
|
74
|
-
export const GetJiraIssueSchema = z.object({
|
|
75
|
-
method: z.literal('tools/call'),
|
|
76
|
-
params: z.object({
|
|
77
|
-
name: z.literal('get_jira_issue'),
|
|
78
|
-
arguments: z.object({
|
|
79
|
-
issueKey: z.string(),
|
|
80
|
-
expand: z.array(z.string()).optional(),
|
|
81
|
-
}),
|
|
82
|
-
}),
|
|
83
|
-
});
|
|
84
|
-
export const GetJiraProjectSchema = z.object({
|
|
85
|
-
method: z.literal('tools/call'),
|
|
86
|
-
params: z.object({
|
|
87
|
-
name: z.literal('get_jira_project'),
|
|
88
|
-
arguments: z.object({
|
|
89
|
-
projectKey: z.string(),
|
|
90
|
-
expand: z.array(z.string()).optional(),
|
|
91
|
-
include_status_counts: z.boolean().optional(),
|
|
92
|
-
}),
|
|
93
|
-
}),
|
|
94
|
-
});
|
|
95
|
-
export const GetJiraBoardSchema = z.object({
|
|
96
|
-
method: z.literal('tools/call'),
|
|
97
|
-
params: z.object({
|
|
98
|
-
name: z.literal('get_jira_board'),
|
|
99
|
-
arguments: z.object({
|
|
100
|
-
boardId: z.number(),
|
|
101
|
-
expand: z.array(z.string()).optional(),
|
|
102
|
-
}),
|
|
103
|
-
}),
|
|
104
|
-
});
|
|
105
|
-
export const SearchJiraIssuesSchema = z.object({
|
|
106
|
-
method: z.literal('tools/call'),
|
|
107
|
-
params: z.object({
|
|
108
|
-
name: z.literal('search_jira_issues'),
|
|
109
|
-
arguments: z.object({
|
|
110
|
-
jql: z.string(),
|
|
111
|
-
startAt: z.number().optional(),
|
|
112
|
-
maxResults: z.number().optional(),
|
|
113
|
-
expand: z.array(z.string()).optional(),
|
|
114
|
-
}),
|
|
115
|
-
}),
|
|
116
|
-
});
|
|
117
|
-
export const ListJiraProjectsSchema = z.object({
|
|
118
|
-
method: z.literal('tools/call'),
|
|
119
|
-
params: z.object({
|
|
120
|
-
name: z.literal('list_jira_projects'),
|
|
121
|
-
arguments: z.object({
|
|
122
|
-
include_status_counts: z.boolean().optional(),
|
|
123
|
-
}),
|
|
124
|
-
}),
|
|
125
|
-
});
|
|
126
|
-
export const ListJiraBoardsSchema = z.object({
|
|
127
|
-
method: z.literal('tools/call'),
|
|
128
|
-
params: z.object({
|
|
129
|
-
name: z.literal('list_jira_boards'),
|
|
130
|
-
arguments: z.object({
|
|
131
|
-
include_sprints: z.boolean().optional(),
|
|
132
|
-
}),
|
|
133
|
-
}),
|
|
134
|
-
});
|
|
135
|
-
export const CreateJiraIssueSchema = z.object({
|
|
136
|
-
method: z.literal('tools/call'),
|
|
137
|
-
params: z.object({
|
|
138
|
-
name: z.literal('create_jira_issue'),
|
|
139
|
-
arguments: z.object({
|
|
140
|
-
projectKey: z.string(),
|
|
141
|
-
summary: z.string(),
|
|
142
|
-
description: z.string().optional(),
|
|
143
|
-
issueType: z.string(),
|
|
144
|
-
priority: z.string().optional(),
|
|
145
|
-
assignee: z.string().optional(),
|
|
146
|
-
labels: z.array(z.string()).optional(),
|
|
147
|
-
customFields: z.record(z.any()).optional(),
|
|
148
|
-
}),
|
|
149
|
-
}),
|
|
150
|
-
});
|
|
151
|
-
export const UpdateJiraIssueSchema = z.object({
|
|
152
|
-
method: z.literal('tools/call'),
|
|
153
|
-
params: z.object({
|
|
154
|
-
name: z.literal('update_jira_issue'),
|
|
155
|
-
arguments: z.object({
|
|
156
|
-
issueKey: z.string(),
|
|
157
|
-
summary: z.string().optional(),
|
|
158
|
-
description: z.string().optional(),
|
|
159
|
-
parent: z.union([z.string(), z.null()]).optional(),
|
|
160
|
-
assignee: z.string().optional(),
|
|
161
|
-
priority: z.string().optional(),
|
|
162
|
-
labels: z.array(z.string()).optional(),
|
|
163
|
-
customFields: z.record(z.any()).optional(),
|
|
164
|
-
}),
|
|
165
|
-
}),
|
|
166
|
-
});
|
|
167
|
-
export const TransitionJiraIssueSchema = z.object({
|
|
168
|
-
method: z.literal('tools/call'),
|
|
169
|
-
params: z.object({
|
|
170
|
-
name: z.literal('transition_jira_issue'),
|
|
171
|
-
arguments: z.object({
|
|
172
|
-
issueKey: z.string(),
|
|
173
|
-
transitionId: z.string(),
|
|
174
|
-
comment: z.string().optional(),
|
|
175
|
-
}),
|
|
176
|
-
}),
|
|
177
|
-
});
|
|
178
|
-
export const AddJiraCommentSchema = z.object({
|
|
179
|
-
method: z.literal('tools/call'),
|
|
180
|
-
params: z.object({
|
|
181
|
-
name: z.literal('add_jira_comment'),
|
|
182
|
-
arguments: z.object({
|
|
183
|
-
issueKey: z.string(),
|
|
184
|
-
body: z.string(),
|
|
185
|
-
}),
|
|
186
|
-
}),
|
|
187
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base formatter for consistent response structure across all entity types
|
|
3
|
-
*/
|
|
4
|
-
export class BaseFormatter {
|
|
5
|
-
/**
|
|
6
|
-
* Format a response with the standard structure
|
|
7
|
-
* @param data The main data for the response
|
|
8
|
-
* @param metadata Optional metadata about the response
|
|
9
|
-
* @param summary Optional summary information
|
|
10
|
-
* @returns A formatted response object
|
|
11
|
-
*/
|
|
12
|
-
static formatResponse(data, metadata, summary) {
|
|
13
|
-
return {
|
|
14
|
-
data,
|
|
15
|
-
...(metadata && { _metadata: metadata }),
|
|
16
|
-
...(summary && { _summary: summary }),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Create metadata for a response
|
|
21
|
-
* @param options Metadata options
|
|
22
|
-
* @returns Response metadata
|
|
23
|
-
*/
|
|
24
|
-
static createMetadata(options) {
|
|
25
|
-
const metadata = {};
|
|
26
|
-
if (options.expansions && options.expansions.length > 0) {
|
|
27
|
-
metadata.expansions = options.expansions;
|
|
28
|
-
}
|
|
29
|
-
if (options.related && Object.keys(options.related).length > 0) {
|
|
30
|
-
metadata.related = options.related;
|
|
31
|
-
}
|
|
32
|
-
if (options.pagination) {
|
|
33
|
-
const { startAt, maxResults, total } = options.pagination;
|
|
34
|
-
metadata.pagination = {
|
|
35
|
-
startAt,
|
|
36
|
-
maxResults,
|
|
37
|
-
total,
|
|
38
|
-
hasMore: startAt + maxResults < total,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
return metadata;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Create a summary for a response
|
|
45
|
-
* @param options Summary options
|
|
46
|
-
* @returns Response summary
|
|
47
|
-
*/
|
|
48
|
-
static createSummary(options) {
|
|
49
|
-
const summary = {};
|
|
50
|
-
if (options.status_counts && Object.keys(options.status_counts).length > 0) {
|
|
51
|
-
summary.status_counts = options.status_counts;
|
|
52
|
-
}
|
|
53
|
-
if (options.suggested_actions && options.suggested_actions.length > 0) {
|
|
54
|
-
summary.suggested_actions = options.suggested_actions;
|
|
55
|
-
}
|
|
56
|
-
return summary;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
-
export class BoardFormatter {
|
|
3
|
-
/**
|
|
4
|
-
* Format a board response with the standard structure and optional expansions
|
|
5
|
-
* @param board The board data
|
|
6
|
-
* @param options Expansion options
|
|
7
|
-
* @returns A formatted board response
|
|
8
|
-
*/
|
|
9
|
-
static formatBoard(board, options = {}) {
|
|
10
|
-
// Create metadata with available expansions
|
|
11
|
-
const metadata = this.createBoardMetadata(board, options);
|
|
12
|
-
// Create summary
|
|
13
|
-
const summary = this.createBoardSummary(board);
|
|
14
|
-
return BaseFormatter.formatResponse(board, metadata, summary);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Create metadata for a board response
|
|
18
|
-
*/
|
|
19
|
-
static createBoardMetadata(board, options) {
|
|
20
|
-
// Determine which expansions are available but not included
|
|
21
|
-
const availableExpansions = [];
|
|
22
|
-
if (!options.sprints && !board.sprints) {
|
|
23
|
-
availableExpansions.push('sprints');
|
|
24
|
-
}
|
|
25
|
-
if (!options.issues) {
|
|
26
|
-
availableExpansions.push('issues');
|
|
27
|
-
}
|
|
28
|
-
if (!options.configuration) {
|
|
29
|
-
availableExpansions.push('configuration');
|
|
30
|
-
}
|
|
31
|
-
// Create related entities map
|
|
32
|
-
const related = {};
|
|
33
|
-
if (board.location?.projectId) {
|
|
34
|
-
related.project = board.location.projectName || `Project ${board.location.projectId}`;
|
|
35
|
-
}
|
|
36
|
-
return BaseFormatter.createMetadata({
|
|
37
|
-
expansions: availableExpansions,
|
|
38
|
-
related
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Create a summary for a board response
|
|
43
|
-
*/
|
|
44
|
-
static createBoardSummary(board) {
|
|
45
|
-
const suggestedActions = [
|
|
46
|
-
{
|
|
47
|
-
text: `View all issues on ${board.name}`
|
|
48
|
-
}
|
|
49
|
-
];
|
|
50
|
-
// Add sprint-related actions if sprints are available
|
|
51
|
-
if (board.sprints && board.sprints.length > 0) {
|
|
52
|
-
const activeSprints = board.sprints.filter(sprint => sprint.state === 'active');
|
|
53
|
-
if (activeSprints.length > 0) {
|
|
54
|
-
suggestedActions.push({
|
|
55
|
-
text: `View active sprint: ${activeSprints[0].name}`
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return BaseFormatter.createSummary({
|
|
60
|
-
suggested_actions: suggestedActions
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
-
export class FilterFormatter {
|
|
3
|
-
/**
|
|
4
|
-
* Format a filter response with the standard structure and optional expansions
|
|
5
|
-
* @param filter The filter data
|
|
6
|
-
* @param options Expansion options
|
|
7
|
-
* @returns A formatted filter response
|
|
8
|
-
*/
|
|
9
|
-
static formatFilter(filter, options = {}) {
|
|
10
|
-
// Create metadata with available expansions
|
|
11
|
-
const metadata = this.createFilterMetadata(filter, options);
|
|
12
|
-
// Create summary
|
|
13
|
-
const summary = this.createFilterSummary(filter);
|
|
14
|
-
return BaseFormatter.formatResponse(filter, metadata, summary);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Create metadata for a filter response
|
|
18
|
-
*/
|
|
19
|
-
static createFilterMetadata(filter, options) {
|
|
20
|
-
// Determine which expansions are available but not included
|
|
21
|
-
const availableExpansions = [];
|
|
22
|
-
if (!options.jql && !filter.jql) {
|
|
23
|
-
availableExpansions.push('jql');
|
|
24
|
-
}
|
|
25
|
-
if (!options.description && !filter.description) {
|
|
26
|
-
availableExpansions.push('description');
|
|
27
|
-
}
|
|
28
|
-
if (!options.permissions && !filter.sharePermissions) {
|
|
29
|
-
availableExpansions.push('permissions');
|
|
30
|
-
}
|
|
31
|
-
if (!options.issue_count && filter.issueCount === undefined) {
|
|
32
|
-
availableExpansions.push('issue_count');
|
|
33
|
-
}
|
|
34
|
-
// Create related entities map
|
|
35
|
-
const related = {
|
|
36
|
-
owner: filter.owner
|
|
37
|
-
};
|
|
38
|
-
return BaseFormatter.createMetadata({
|
|
39
|
-
expansions: availableExpansions,
|
|
40
|
-
related
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Create a summary for a filter response
|
|
45
|
-
*/
|
|
46
|
-
static createFilterSummary(filter) {
|
|
47
|
-
const suggestedActions = [
|
|
48
|
-
{
|
|
49
|
-
text: `View filter results: ${filter.name}`
|
|
50
|
-
}
|
|
51
|
-
];
|
|
52
|
-
if (filter.jql) {
|
|
53
|
-
suggestedActions.push({
|
|
54
|
-
text: `Edit JQL: ${filter.name}`
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
if (!filter.favourite) {
|
|
58
|
-
suggestedActions.push({
|
|
59
|
-
text: `Add to favorites`
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
return BaseFormatter.createSummary({
|
|
63
|
-
suggested_actions: suggestedActions
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export * from './base-formatter.js';
|
|
2
|
-
export * from './issue-formatter.js';
|
|
3
|
-
export * from './project-formatter.js';
|
|
4
|
-
export * from './search-formatter.js';
|
|
5
|
-
export * from './board-formatter.js';
|
|
6
|
-
export * from './sprint-formatter.js';
|
|
7
|
-
export * from './filter-formatter.js';
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
-
export class IssueFormatter {
|
|
3
|
-
/**
|
|
4
|
-
* Format an issue response with the standard structure and optional expansions
|
|
5
|
-
* @param issue The issue data
|
|
6
|
-
* @param options Expansion options
|
|
7
|
-
* @param transitions Optional transitions data (if requested)
|
|
8
|
-
* @returns A formatted issue response
|
|
9
|
-
*/
|
|
10
|
-
static formatIssue(issue, options = {}, transitions) {
|
|
11
|
-
// Create metadata with available expansions
|
|
12
|
-
const metadata = this.createIssueMetadata(issue, options, transitions);
|
|
13
|
-
// Create summary with status and suggested actions
|
|
14
|
-
const summary = this.createIssueSummary(issue, transitions);
|
|
15
|
-
return BaseFormatter.formatResponse(issue, metadata, summary);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Create metadata for an issue response
|
|
19
|
-
*/
|
|
20
|
-
static createIssueMetadata(issue, options, transitions) {
|
|
21
|
-
// Determine which expansions are available but not included
|
|
22
|
-
const availableExpansions = [];
|
|
23
|
-
if (!options.comments && issue.comments === undefined) {
|
|
24
|
-
availableExpansions.push('comments');
|
|
25
|
-
}
|
|
26
|
-
if (!options.transitions && transitions === undefined) {
|
|
27
|
-
availableExpansions.push('transitions');
|
|
28
|
-
}
|
|
29
|
-
if (!options.attachments && issue.attachments === undefined) {
|
|
30
|
-
availableExpansions.push('attachments');
|
|
31
|
-
}
|
|
32
|
-
if (!options.related_issues) {
|
|
33
|
-
availableExpansions.push('related_issues');
|
|
34
|
-
}
|
|
35
|
-
if (!options.history) {
|
|
36
|
-
availableExpansions.push('history');
|
|
37
|
-
}
|
|
38
|
-
// Create related entities map
|
|
39
|
-
const related = {};
|
|
40
|
-
if (issue.parent) {
|
|
41
|
-
related.parent = issue.parent;
|
|
42
|
-
}
|
|
43
|
-
// Extract related issues from issue links
|
|
44
|
-
const relatedIssues = issue.issueLinks
|
|
45
|
-
.map(link => link.outward || link.inward)
|
|
46
|
-
.filter((key) => key !== null);
|
|
47
|
-
if (relatedIssues.length > 0) {
|
|
48
|
-
related.linked_issues = relatedIssues;
|
|
49
|
-
}
|
|
50
|
-
return BaseFormatter.createMetadata({
|
|
51
|
-
expansions: availableExpansions,
|
|
52
|
-
related
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Create a summary for an issue response
|
|
57
|
-
*/
|
|
58
|
-
static createIssueSummary(issue, transitions) {
|
|
59
|
-
const suggestedActions = [];
|
|
60
|
-
// Add suggested actions based on available transitions
|
|
61
|
-
if (transitions && transitions.length > 0) {
|
|
62
|
-
// Common transitions to suggest
|
|
63
|
-
const commonTransitions = ['Done', 'In Progress', 'To Do', 'Closed', 'Resolved'];
|
|
64
|
-
for (const transitionName of commonTransitions) {
|
|
65
|
-
const transition = transitions.find(t => t.name === transitionName);
|
|
66
|
-
if (transition) {
|
|
67
|
-
suggestedActions.push({
|
|
68
|
-
text: `Move to ${transitionName}`,
|
|
69
|
-
action_id: transition.id
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// Add assignment suggestion if not assigned
|
|
75
|
-
if (!issue.assignee) {
|
|
76
|
-
suggestedActions.push({
|
|
77
|
-
text: 'Assign to team member'
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return BaseFormatter.createSummary({
|
|
81
|
-
suggested_actions: suggestedActions
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
-
export class ProjectFormatter {
|
|
3
|
-
/**
|
|
4
|
-
* Format a project response with the standard structure and optional expansions
|
|
5
|
-
* @param project The project data
|
|
6
|
-
* @param options Expansion options
|
|
7
|
-
* @returns A formatted project response
|
|
8
|
-
*/
|
|
9
|
-
static formatProject(project, options = {}) {
|
|
10
|
-
// Create metadata with available expansions
|
|
11
|
-
const metadata = this.createProjectMetadata(project, options);
|
|
12
|
-
// Create summary with status counts
|
|
13
|
-
const summary = this.createProjectSummary(project);
|
|
14
|
-
return BaseFormatter.formatResponse(project, metadata, summary);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Create metadata for a project response
|
|
18
|
-
*/
|
|
19
|
-
static createProjectMetadata(project, options) {
|
|
20
|
-
// Determine which expansions are available but not included
|
|
21
|
-
const availableExpansions = [];
|
|
22
|
-
if (!options.boards) {
|
|
23
|
-
availableExpansions.push('boards');
|
|
24
|
-
}
|
|
25
|
-
if (!options.components) {
|
|
26
|
-
availableExpansions.push('components');
|
|
27
|
-
}
|
|
28
|
-
if (!options.versions) {
|
|
29
|
-
availableExpansions.push('versions');
|
|
30
|
-
}
|
|
31
|
-
if (!options.recent_issues) {
|
|
32
|
-
availableExpansions.push('recent_issues');
|
|
33
|
-
}
|
|
34
|
-
return BaseFormatter.createMetadata({
|
|
35
|
-
expansions: availableExpansions
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Create a summary for a project response
|
|
40
|
-
*/
|
|
41
|
-
static createProjectSummary(project) {
|
|
42
|
-
const suggestedActions = [
|
|
43
|
-
{
|
|
44
|
-
text: `View all issues in ${project.key}`
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
text: `Create issue in ${project.key}`
|
|
48
|
-
}
|
|
49
|
-
];
|
|
50
|
-
return BaseFormatter.createSummary({
|
|
51
|
-
status_counts: project.status_counts,
|
|
52
|
-
suggested_actions: suggestedActions
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
-
export class SearchFormatter {
|
|
3
|
-
/**
|
|
4
|
-
* Format a search response with the standard structure and optional expansions
|
|
5
|
-
* @param searchResult The search result data
|
|
6
|
-
* @param options Expansion options
|
|
7
|
-
* @returns A formatted search response
|
|
8
|
-
*/
|
|
9
|
-
static formatSearchResult(searchResult, options = {}) {
|
|
10
|
-
// Create metadata with available expansions and pagination
|
|
11
|
-
const metadata = this.createSearchMetadata(searchResult, options);
|
|
12
|
-
// Create summary with status counts
|
|
13
|
-
const summary = this.createSearchSummary(searchResult);
|
|
14
|
-
return BaseFormatter.formatResponse(searchResult, metadata, summary);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Create metadata for a search response
|
|
18
|
-
*/
|
|
19
|
-
static createSearchMetadata(searchResult, options) {
|
|
20
|
-
// Determine which expansions are available but not included
|
|
21
|
-
const availableExpansions = [];
|
|
22
|
-
if (!options.issue_details) {
|
|
23
|
-
availableExpansions.push('issue_details');
|
|
24
|
-
}
|
|
25
|
-
if (!options.transitions) {
|
|
26
|
-
availableExpansions.push('transitions');
|
|
27
|
-
}
|
|
28
|
-
if (!options.comments_preview) {
|
|
29
|
-
availableExpansions.push('comments_preview');
|
|
30
|
-
}
|
|
31
|
-
return BaseFormatter.createMetadata({
|
|
32
|
-
expansions: availableExpansions,
|
|
33
|
-
pagination: {
|
|
34
|
-
startAt: searchResult.pagination.startAt,
|
|
35
|
-
maxResults: searchResult.pagination.maxResults,
|
|
36
|
-
total: searchResult.pagination.total
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Create a summary for a search response
|
|
42
|
-
*/
|
|
43
|
-
static createSearchSummary(searchResult) {
|
|
44
|
-
// Count issues by status
|
|
45
|
-
const statusCounts = {};
|
|
46
|
-
for (const issue of searchResult.issues) {
|
|
47
|
-
const status = issue.status;
|
|
48
|
-
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
49
|
-
}
|
|
50
|
-
const suggestedActions = [];
|
|
51
|
-
// Add pagination actions if there are more results
|
|
52
|
-
if (searchResult.pagination.hasMore) {
|
|
53
|
-
suggestedActions.push({
|
|
54
|
-
text: 'Load more results'
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return BaseFormatter.createSummary({
|
|
58
|
-
status_counts: statusCounts,
|
|
59
|
-
suggested_actions: suggestedActions
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { BaseFormatter } from './base-formatter.js';
|
|
2
|
-
export class SprintFormatter {
|
|
3
|
-
/**
|
|
4
|
-
* Format a sprint response with the standard structure and optional expansions
|
|
5
|
-
* @param sprint The sprint data
|
|
6
|
-
* @param options Expansion options
|
|
7
|
-
* @returns A formatted sprint response
|
|
8
|
-
*/
|
|
9
|
-
static formatSprint(sprint, options = {}) {
|
|
10
|
-
// Create metadata with available expansions
|
|
11
|
-
const metadata = this.createSprintMetadata(sprint, options);
|
|
12
|
-
// Create summary
|
|
13
|
-
const summary = this.createSprintSummary(sprint);
|
|
14
|
-
return BaseFormatter.formatResponse(sprint, metadata, summary);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Format a list of sprints
|
|
18
|
-
* @param sprints Array of sprint data
|
|
19
|
-
* @param pagination Pagination information
|
|
20
|
-
* @returns A formatted response with sprints array
|
|
21
|
-
*/
|
|
22
|
-
static formatSprintList(sprints, pagination) {
|
|
23
|
-
// Create metadata
|
|
24
|
-
const metadata = {};
|
|
25
|
-
if (pagination) {
|
|
26
|
-
metadata.pagination = {
|
|
27
|
-
startAt: pagination.startAt,
|
|
28
|
-
maxResults: pagination.maxResults,
|
|
29
|
-
total: pagination.total,
|
|
30
|
-
hasMore: pagination.startAt + pagination.maxResults < pagination.total,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
// Create summary with status counts
|
|
34
|
-
const statusCounts = {
|
|
35
|
-
future: 0,
|
|
36
|
-
active: 0,
|
|
37
|
-
closed: 0,
|
|
38
|
-
};
|
|
39
|
-
sprints.forEach(sprint => {
|
|
40
|
-
if (sprint.state && statusCounts[sprint.state] !== undefined) {
|
|
41
|
-
statusCounts[sprint.state]++;
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
const summary = {
|
|
45
|
-
status_counts: statusCounts,
|
|
46
|
-
suggested_actions: [
|
|
47
|
-
{ text: 'Create new sprint', action_id: 'create_sprint' },
|
|
48
|
-
],
|
|
49
|
-
};
|
|
50
|
-
return BaseFormatter.formatResponse(sprints, metadata, summary);
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Create metadata for a sprint response
|
|
54
|
-
*/
|
|
55
|
-
static createSprintMetadata(sprint, options) {
|
|
56
|
-
// Determine which expansions are available but not included
|
|
57
|
-
const availableExpansions = [];
|
|
58
|
-
if (!options.issues && !sprint.issues) {
|
|
59
|
-
availableExpansions.push('issues');
|
|
60
|
-
}
|
|
61
|
-
if (!options.report && !sprint.report) {
|
|
62
|
-
availableExpansions.push('report');
|
|
63
|
-
}
|
|
64
|
-
// Create related entities map
|
|
65
|
-
const related = {
|
|
66
|
-
board: sprint.boardId.toString(),
|
|
67
|
-
};
|
|
68
|
-
return BaseFormatter.createMetadata({
|
|
69
|
-
expansions: availableExpansions,
|
|
70
|
-
related
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Create a summary for a sprint response
|
|
75
|
-
*/
|
|
76
|
-
static createSprintSummary(sprint) {
|
|
77
|
-
const suggestedActions = [];
|
|
78
|
-
// Add suggested actions based on sprint state
|
|
79
|
-
switch (sprint.state) {
|
|
80
|
-
case 'future':
|
|
81
|
-
suggestedActions.push({ text: 'Start Sprint', action_id: 'start_sprint' });
|
|
82
|
-
suggestedActions.push({ text: 'Add Issues to Sprint', action_id: 'add_issues' });
|
|
83
|
-
suggestedActions.push({ text: 'Edit Sprint', action_id: 'update_sprint' });
|
|
84
|
-
break;
|
|
85
|
-
case 'active':
|
|
86
|
-
suggestedActions.push({ text: 'Complete Sprint', action_id: 'complete_sprint' });
|
|
87
|
-
suggestedActions.push({ text: 'Add Issues to Sprint', action_id: 'add_issues' });
|
|
88
|
-
suggestedActions.push({ text: 'Remove Issues from Sprint', action_id: 'remove_issues' });
|
|
89
|
-
suggestedActions.push({ text: 'Edit Sprint', action_id: 'update_sprint' });
|
|
90
|
-
break;
|
|
91
|
-
case 'closed':
|
|
92
|
-
suggestedActions.push({ text: 'View Sprint Report', action_id: 'view_report' });
|
|
93
|
-
suggestedActions.push({ text: 'Create New Sprint', action_id: 'create_sprint' });
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
// Add status counts if issues are available
|
|
97
|
-
const statusCounts = {};
|
|
98
|
-
if (sprint.issues && sprint.issues.length > 0) {
|
|
99
|
-
sprint.issues.forEach(issue => {
|
|
100
|
-
if (!statusCounts[issue.status]) {
|
|
101
|
-
statusCounts[issue.status] = 0;
|
|
102
|
-
}
|
|
103
|
-
statusCounts[issue.status]++;
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
return BaseFormatter.createSummary({
|
|
107
|
-
status_counts: Object.keys(statusCounts).length > 0 ? statusCounts : undefined,
|
|
108
|
-
suggested_actions: suggestedActions
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|