@stubbedev/atlassian-mcp 0.1.5 → 0.1.7
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 +3 -1
- package/dist/bitbucket.js +89 -24
- package/dist/index.js +4 -3
- package/dist/jira.js +36 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
52
52
|
| `bitbucket_decline_pr` | Decline a pull request |
|
|
53
53
|
| `bitbucket_get_pr_comments` | Get PR comment threads in bulk, including task-style BLOCKER comments and blocker counts |
|
|
54
54
|
| `bitbucket_add_pr_comment` | Add a top-level PR comment or reply to an existing comment |
|
|
55
|
-
| `bitbucket_update_pr_comment` | Update comment text,
|
|
55
|
+
| `bitbucket_update_pr_comment` | Update comment text/severity, resolve or reopen normal threads via `threadResolved`, and resolve/reopen BLOCKER tasks via `state` (strictly enforced) |
|
|
56
56
|
| `bitbucket_delete_pr_comment` | Delete a PR comment by comment ID |
|
|
57
57
|
| `bitbucket_get_pr_commits` | List commits included in a pull request |
|
|
58
58
|
| `bitbucket_get_branches` | List branches in a repository |
|
|
@@ -76,6 +76,8 @@ All list tools support `limit` and `start`/`startAt` for pagination.
|
|
|
76
76
|
- "show review comments on PR 42" → `bitbucket_get_pr_comments`
|
|
77
77
|
- "give me one full overview of PR 42" → `bitbucket_get_pr_overview`
|
|
78
78
|
- "how many open blockers are on PR 42" → `bitbucket_get_pr_comments` with `severity=BLOCKER` and `countOnly=true`
|
|
79
|
+
- "resolve this review thread on PR 42" → `bitbucket_update_pr_comment` with `threadResolved=true`
|
|
80
|
+
- "resolve this blocker task on PR 42" → `bitbucket_update_pr_comment` with `severity=BLOCKER` and `state=RESOLVED`
|
|
79
81
|
- "move FOO-123 to In Progress" → `jira_transition_issue` with `transitionName="In Progress"`
|
|
80
82
|
- "find bugs assigned to me in PAY project" → `jira_search_issues`
|
|
81
83
|
- "give me my coding context for this branch" → `get_dev_context`
|
package/dist/bitbucket.js
CHANGED
|
@@ -39,8 +39,11 @@ function formatCommentThread(comment, indent = '') {
|
|
|
39
39
|
const date = comment.createdDate ? ` (${formatDate(comment.createdDate)})` : '';
|
|
40
40
|
const state = comment.state ?? 'OPEN';
|
|
41
41
|
const severity = comment.severity ?? 'NORMAL';
|
|
42
|
+
const threadStatus = comment.threadResolved !== undefined
|
|
43
|
+
? ` thread=${comment.threadResolved ? 'RESOLVED' : 'OPEN'}`
|
|
44
|
+
: '';
|
|
42
45
|
const lines = [
|
|
43
|
-
`${indent}#${comment.id} [${state}/${severity}] ${author}${date} (v${comment.version})`,
|
|
46
|
+
`${indent}#${comment.id} [${state}/${severity}${threadStatus}] ${author}${date} (v${comment.version})`,
|
|
44
47
|
`${indent}${comment.text}`,
|
|
45
48
|
];
|
|
46
49
|
if (comment.comments && comment.comments.length > 0) {
|
|
@@ -51,6 +54,11 @@ function formatCommentThread(comment, indent = '') {
|
|
|
51
54
|
return lines;
|
|
52
55
|
}
|
|
53
56
|
function commentMatchesState(comment, state) {
|
|
57
|
+
if (state !== 'PENDING' && (comment.severity ?? 'NORMAL') !== 'BLOCKER' && comment.threadResolved !== undefined) {
|
|
58
|
+
const threadState = comment.threadResolved ? 'RESOLVED' : 'OPEN';
|
|
59
|
+
if (threadState === state)
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
54
62
|
const currentState = comment.state ?? 'OPEN';
|
|
55
63
|
if (currentState === state)
|
|
56
64
|
return true;
|
|
@@ -67,11 +75,31 @@ function commentMatchesSeverity(comment, severity) {
|
|
|
67
75
|
function uniqueCommentsFromActivities(activities) {
|
|
68
76
|
const byId = new Map();
|
|
69
77
|
for (const activity of activities) {
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
const comment = activity.comment;
|
|
79
|
+
if (!comment)
|
|
80
|
+
continue;
|
|
81
|
+
const existing = byId.get(comment.id);
|
|
82
|
+
if (!existing) {
|
|
83
|
+
byId.set(comment.id, comment);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const commentVersion = comment.version ?? -1;
|
|
87
|
+
const existingVersion = existing.version ?? -1;
|
|
88
|
+
if (commentVersion > existingVersion) {
|
|
89
|
+
byId.set(comment.id, comment);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (commentVersion === existingVersion) {
|
|
93
|
+
const commentUpdated = comment.updatedDate ?? comment.createdDate ?? 0;
|
|
94
|
+
const existingUpdated = existing.updatedDate ?? existing.createdDate ?? 0;
|
|
95
|
+
if (commentUpdated > existingUpdated) {
|
|
96
|
+
byId.set(comment.id, comment);
|
|
97
|
+
}
|
|
72
98
|
}
|
|
73
99
|
}
|
|
74
|
-
return Array.from(byId.values())
|
|
100
|
+
return Array.from(byId.values())
|
|
101
|
+
.filter((comment) => !comment.deleted)
|
|
102
|
+
.sort((a, b) => (a.createdDate ?? 0) - (b.createdDate ?? 0));
|
|
75
103
|
}
|
|
76
104
|
function pageHint(data) {
|
|
77
105
|
return data.isLastPage ? '' : ` (use start=${data.nextPageStart} for next page)`;
|
|
@@ -158,6 +186,13 @@ export class BitbucketClient {
|
|
|
158
186
|
Accept: 'application/json',
|
|
159
187
|
};
|
|
160
188
|
}
|
|
189
|
+
pullRequestUrl(projectKey, repoSlug, prId, pr) {
|
|
190
|
+
const apiUrl = pr?.links?.self?.[0]?.href?.trim();
|
|
191
|
+
if (apiUrl) {
|
|
192
|
+
return apiUrl;
|
|
193
|
+
}
|
|
194
|
+
return `${this.baseUrl}/projects/${encodeURIComponent(projectKey)}/repos/${encodeURIComponent(repoSlug)}/pull-requests/${prId}`;
|
|
195
|
+
}
|
|
161
196
|
resolveProjectAndRepo(projectKey, repoSlug) {
|
|
162
197
|
if (projectKey && repoSlug)
|
|
163
198
|
return { projectKey, repoSlug };
|
|
@@ -375,20 +410,21 @@ export class BitbucketClient {
|
|
|
375
410
|
const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests`, body);
|
|
376
411
|
if (!data)
|
|
377
412
|
return text('Pull request created.');
|
|
378
|
-
const url = data.
|
|
379
|
-
return text(`Created PR #${data.id}: "${data.title}"
|
|
413
|
+
const url = this.pullRequestUrl(projectKey, repoSlug, data.id, data);
|
|
414
|
+
return text(`Created PR #${data.id}: "${data.title}"\n${url}`);
|
|
380
415
|
}
|
|
381
416
|
async approvePr(args) {
|
|
382
417
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
383
418
|
const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/approve`);
|
|
419
|
+
const url = this.pullRequestUrl(projectKey, repoSlug, args.prId);
|
|
384
420
|
if (!data)
|
|
385
|
-
return text(`Approved PR #${args.prId}
|
|
386
|
-
return text(`Approved PR #${args.prId} as ${data.user.displayName}
|
|
421
|
+
return text(`Approved PR #${args.prId}.\n${url}`);
|
|
422
|
+
return text(`Approved PR #${args.prId} as ${data.user.displayName}.\n${url}`);
|
|
387
423
|
}
|
|
388
424
|
async unapprovePr(args) {
|
|
389
425
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
390
426
|
await this.request('DELETE', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/approve`);
|
|
391
|
-
return text(`Approval removed from PR #${args.prId}
|
|
427
|
+
return text(`Approval removed from PR #${args.prId}.\n${this.pullRequestUrl(projectKey, repoSlug, args.prId)}`);
|
|
392
428
|
}
|
|
393
429
|
async declinePr(args) {
|
|
394
430
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
@@ -400,8 +436,8 @@ export class BitbucketClient {
|
|
|
400
436
|
body.message = args.message;
|
|
401
437
|
const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/decline`, body);
|
|
402
438
|
if (!data)
|
|
403
|
-
return text(`Declined PR #${args.prId}
|
|
404
|
-
return text(`Declined PR #${data.id}: "${data.title}"
|
|
439
|
+
return text(`Declined PR #${args.prId}.\n${this.pullRequestUrl(projectKey, repoSlug, args.prId)}`);
|
|
440
|
+
return text(`Declined PR #${data.id}: "${data.title}".\n${this.pullRequestUrl(projectKey, repoSlug, data.id, data)}`);
|
|
405
441
|
}
|
|
406
442
|
async mergePr(args) {
|
|
407
443
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
@@ -415,8 +451,8 @@ export class BitbucketClient {
|
|
|
415
451
|
body.message = args.message;
|
|
416
452
|
const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/merge`, body);
|
|
417
453
|
if (!data)
|
|
418
|
-
return text(`Merged PR #${args.prId}
|
|
419
|
-
return text(`Merged PR #${data.id}: "${data.title}" (${data.fromRef.displayId} → ${data.toRef.displayId})
|
|
454
|
+
return text(`Merged PR #${args.prId}.\n${this.pullRequestUrl(projectKey, repoSlug, args.prId)}`);
|
|
455
|
+
return text(`Merged PR #${data.id}: "${data.title}" (${data.fromRef.displayId} → ${data.toRef.displayId}).\n${this.pullRequestUrl(projectKey, repoSlug, data.id, data)}`);
|
|
420
456
|
}
|
|
421
457
|
async getBranches(args) {
|
|
422
458
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
@@ -534,26 +570,55 @@ export class BitbucketClient {
|
|
|
534
570
|
}
|
|
535
571
|
async updatePrComment(args) {
|
|
536
572
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
537
|
-
if (!args.text && !args.state && !args.severity) {
|
|
538
|
-
throw new Error('At least one field is required: text, state, or
|
|
573
|
+
if (!args.text && !args.state && !args.severity && args.threadResolved === undefined) {
|
|
574
|
+
throw new Error('At least one field is required: text, state, severity, or threadResolved');
|
|
539
575
|
}
|
|
540
576
|
const current = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}`);
|
|
541
577
|
if (!current)
|
|
542
578
|
throw new Error(`Comment #${args.commentId} not found.`);
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
579
|
+
const targetSeverity = args.severity ?? current.severity ?? 'NORMAL';
|
|
580
|
+
if (args.state && targetSeverity !== 'BLOCKER') {
|
|
581
|
+
throw new Error('state is only supported for BLOCKER comments (tasks). Use threadResolved for normal comment threads.');
|
|
582
|
+
}
|
|
583
|
+
if (args.threadResolved !== undefined && targetSeverity === 'BLOCKER') {
|
|
584
|
+
throw new Error('threadResolved is only supported for normal comments. Use state for BLOCKER comment tasks.');
|
|
585
|
+
}
|
|
586
|
+
const commentPath = (targetSeverity === 'BLOCKER' || current.severity === 'BLOCKER')
|
|
587
|
+
? `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/blocker-comments/${args.commentId}`
|
|
588
|
+
: `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}`;
|
|
589
|
+
const buildBody = (version) => {
|
|
590
|
+
const body = { version };
|
|
591
|
+
if (args.text !== undefined)
|
|
592
|
+
body.text = validateCommentText(args.text);
|
|
593
|
+
if (args.state && targetSeverity === 'BLOCKER')
|
|
594
|
+
body.state = args.state;
|
|
595
|
+
if (args.severity)
|
|
596
|
+
body.severity = args.severity;
|
|
597
|
+
if (args.threadResolved !== undefined) {
|
|
598
|
+
body.threadResolved = args.threadResolved;
|
|
599
|
+
}
|
|
600
|
+
return body;
|
|
546
601
|
};
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
602
|
+
let updated;
|
|
603
|
+
try {
|
|
604
|
+
updated = await this.request('PUT', commentPath, buildBody(current.version));
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
608
|
+
if (!message.includes('Bitbucket 409'))
|
|
609
|
+
throw error;
|
|
610
|
+
const latest = await this.request('GET', commentPath);
|
|
611
|
+
if (!latest)
|
|
612
|
+
throw error;
|
|
613
|
+
updated = await this.request('PUT', commentPath, buildBody(latest.version));
|
|
614
|
+
}
|
|
552
615
|
if (!updated)
|
|
553
616
|
return text(`Comment #${args.commentId} updated.`);
|
|
554
617
|
const state = updated.state ?? current.state ?? 'OPEN';
|
|
555
618
|
const severity = updated.severity ?? current.severity ?? 'NORMAL';
|
|
556
|
-
|
|
619
|
+
const threadResolved = updated.threadResolved ?? current.threadResolved;
|
|
620
|
+
const threadStatus = threadResolved === undefined ? '' : `, thread=${threadResolved ? 'RESOLVED' : 'OPEN'}`;
|
|
621
|
+
return text(`Comment #${updated.id} updated (${state}/${severity}${threadStatus}).`);
|
|
557
622
|
}
|
|
558
623
|
async deletePrComment(args) {
|
|
559
624
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
package/dist/index.js
CHANGED
|
@@ -505,7 +505,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
505
505
|
repo: { type: 'string', description: 'Alias for repoSlug' },
|
|
506
506
|
prId: { type: 'number', description: 'Pull request number (PR ID)' },
|
|
507
507
|
path: { type: 'string', description: 'Optional file path filter, e.g. "src/index.ts"' },
|
|
508
|
-
state: { type: 'string', enum: ['OPEN', 'RESOLVED', 'PENDING'], description: '
|
|
508
|
+
state: { type: 'string', enum: ['OPEN', 'RESOLVED', 'PENDING'], description: 'State filter. For normal comments this maps to threadResolved (OPEN/RESOLVED). For BLOCKER tasks this uses task state (OPEN/RESOLVED; PENDING allowed only for non-BLOCKER).', default: 'OPEN' },
|
|
509
509
|
severity: { type: 'string', enum: ['ALL', 'NORMAL', 'BLOCKER'], description: 'Comment severity filter. BLOCKER means task/checklist-style review comments.', default: 'ALL' },
|
|
510
510
|
countOnly: { type: 'boolean', description: 'When true with severity=BLOCKER, returns counts instead of comment bodies', default: false },
|
|
511
511
|
limit: { type: 'number', description: 'Max items per page (default 50)', default: 50 },
|
|
@@ -533,7 +533,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
533
533
|
},
|
|
534
534
|
{
|
|
535
535
|
name: 'bitbucket_update_pr_comment',
|
|
536
|
-
description: 'Use when you want to edit PR comments, resolve/reopen
|
|
536
|
+
description: 'Use when you want to edit PR comments, resolve/reopen normal discussion threads, or manage task-style BLOCKER comments. Hint: for normal comments, resolve/reopen means threadResolved; for BLOCKER tasks, resolve/reopen uses state. Keep comments concise, plain text, and free of filler. Never include emojis. You can pass projectKey/repoSlug or project/repo.',
|
|
537
537
|
inputSchema: {
|
|
538
538
|
type: 'object',
|
|
539
539
|
properties: {
|
|
@@ -544,7 +544,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
544
544
|
prId: { type: 'number', description: 'Pull request number (PR ID)' },
|
|
545
545
|
commentId: { type: 'number', description: 'Comment ID to update' },
|
|
546
546
|
text: { type: 'string', description: 'New concise comment text only. No filler. Do not include emojis. (optional)' },
|
|
547
|
-
state: { type: 'string', enum: ['OPEN', 'RESOLVED'], description: '
|
|
547
|
+
state: { type: 'string', enum: ['OPEN', 'RESOLVED'], description: 'Task state for BLOCKER comments only (optional). Rejected for normal comments; use threadResolved instead.' },
|
|
548
|
+
threadResolved: { type: 'boolean', description: 'Resolve/reopen normal comment threads in Bitbucket UI only (optional). Rejected for BLOCKER comments; use state instead.' },
|
|
548
549
|
severity: { type: 'string', enum: ['NORMAL', 'BLOCKER'], description: 'Comment severity (optional). BLOCKER marks it as a task/checklist item.' },
|
|
549
550
|
},
|
|
550
551
|
required: ['prId', 'commentId'],
|
package/dist/jira.js
CHANGED
|
@@ -98,6 +98,18 @@ export class JiraClient {
|
|
|
98
98
|
Accept: 'application/json',
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
+
issueUrl(issueKey) {
|
|
102
|
+
return `${this.baseUrl}/browse/${encodeURIComponent(issueKey)}`;
|
|
103
|
+
}
|
|
104
|
+
projectUrl(projectKey) {
|
|
105
|
+
return `${this.baseUrl}/projects/${encodeURIComponent(projectKey)}`;
|
|
106
|
+
}
|
|
107
|
+
boardUrl(boardId) {
|
|
108
|
+
return `${this.baseUrl}/secure/RapidBoard.jspa?rapidView=${boardId}`;
|
|
109
|
+
}
|
|
110
|
+
sprintUrl(boardId, sprintId) {
|
|
111
|
+
return `${this.boardUrl(boardId)}&sprint=${sprintId}`;
|
|
112
|
+
}
|
|
101
113
|
async requestWithBase(apiBase, method, path, body) {
|
|
102
114
|
const url = `${this.baseUrl}${apiBase}${path}`;
|
|
103
115
|
const opts = { method, headers: this.headers };
|
|
@@ -208,7 +220,7 @@ export class JiraClient {
|
|
|
208
220
|
return text('No results.');
|
|
209
221
|
const lines = data.issues.map((i, idx) => {
|
|
210
222
|
const assignee = i.fields.assignee?.displayName ?? 'Unassigned';
|
|
211
|
-
return `${startAt + idx + 1}. [${i.key}] ${i.fields.summary} | ${i.fields.status.name} | ${assignee}`;
|
|
223
|
+
return `${startAt + idx + 1}. [${i.key}] ${i.fields.summary} | ${i.fields.status.name} | ${assignee} | ${this.issueUrl(i.key)}`;
|
|
212
224
|
});
|
|
213
225
|
const page = pagination(data.total, startAt, data.issues.length);
|
|
214
226
|
return text(`Found ${data.total} issues${page}:\n${lines.join('\n')}`);
|
|
@@ -225,7 +237,7 @@ export class JiraClient {
|
|
|
225
237
|
const data = await this.request('GET', `/project?maxResults=${limit}`);
|
|
226
238
|
if (!data || data.length === 0)
|
|
227
239
|
return text('No projects found.');
|
|
228
|
-
const lines = data.map((p, i) => `${i + 1}. [${p.key}] ${p.name} (${p.projectTypeKey})`);
|
|
240
|
+
const lines = data.map((p, i) => `${i + 1}. [${p.key}] ${p.name} (${p.projectTypeKey}) | ${this.projectUrl(p.key)}`);
|
|
229
241
|
return text(`${data.length} project(s):\n${lines.join('\n')}`);
|
|
230
242
|
}
|
|
231
243
|
async getIssueTypes(args) {
|
|
@@ -253,11 +265,11 @@ export class JiraClient {
|
|
|
253
265
|
const lines = data.values.map((s, i) => {
|
|
254
266
|
const window = [s.startDate?.slice(0, 10), s.endDate?.slice(0, 10)].filter(Boolean).join(' -> ');
|
|
255
267
|
const goal = s.goal?.trim() ? ` | Goal: ${s.goal}` : '';
|
|
256
|
-
return `${startAt + i + 1}. [${s.id}] ${s.name} | ${s.state}${window ? ` | ${window}` : ''}${goal}`;
|
|
268
|
+
return `${startAt + i + 1}. [${s.id}] ${s.name} | ${s.state}${window ? ` | ${window}` : ''}${goal} | ${this.sprintUrl(boardId, s.id)}`;
|
|
257
269
|
});
|
|
258
270
|
const rangeEnd = startAt + data.values.length;
|
|
259
271
|
const page = data.isLast ? '' : ` (showing ${startAt + 1}-${rangeEnd}, use startAt=${rangeEnd} for next page)`;
|
|
260
|
-
return text(`Sprints for board ${boardId}${page}:\n${lines.join('\n')}`);
|
|
272
|
+
return text(`Sprints for board ${boardId}${page}:\nBoard URL: ${this.boardUrl(boardId)}\n${lines.join('\n')}`);
|
|
261
273
|
}
|
|
262
274
|
async searchUsers(args) {
|
|
263
275
|
const params = new URLSearchParams({
|
|
@@ -305,6 +317,7 @@ export class JiraClient {
|
|
|
305
317
|
const f = issue.fields;
|
|
306
318
|
const lines = [
|
|
307
319
|
`Issue: ${issue.key} — ${f.summary}`,
|
|
320
|
+
`URL: ${this.issueUrl(issue.key)}`,
|
|
308
321
|
`Status: ${f.status.name}`,
|
|
309
322
|
`Type: ${f.issuetype.name}`,
|
|
310
323
|
`Priority: ${f.priority?.name ?? 'None'}`,
|
|
@@ -400,12 +413,13 @@ export class JiraClient {
|
|
|
400
413
|
const issueBySprint = new Map(sprintIssueData.map((entry) => [entry.sprintId, entry.issues]));
|
|
401
414
|
const lines = [
|
|
402
415
|
`Board: [${args.boardId}] ${board?.name ?? '(unknown)'} | ${board?.type ?? '(unknown type)'}`,
|
|
416
|
+
`URL: ${this.boardUrl(args.boardId)}`,
|
|
403
417
|
`Sprints: ${sprints.values.length}`,
|
|
404
418
|
'',
|
|
405
419
|
];
|
|
406
420
|
sprints.values.forEach((sprint, idx) => {
|
|
407
421
|
const window = [sprint.startDate?.slice(0, 10), sprint.endDate?.slice(0, 10)].filter(Boolean).join(' -> ');
|
|
408
|
-
lines.push(`${sprintStartAt + idx + 1}. [${sprint.id}] ${sprint.name} | ${sprint.state}${window ? ` | ${window}` : ''}`);
|
|
422
|
+
lines.push(`${sprintStartAt + idx + 1}. [${sprint.id}] ${sprint.name} | ${sprint.state}${window ? ` | ${window}` : ''} | ${this.sprintUrl(args.boardId, sprint.id)}`);
|
|
409
423
|
if (sprint.goal?.trim()) {
|
|
410
424
|
lines.push(` Goal: ${sprint.goal}`);
|
|
411
425
|
}
|
|
@@ -415,7 +429,7 @@ export class JiraClient {
|
|
|
415
429
|
lines.push(` Issues: ${issueData?.total ?? 0}`);
|
|
416
430
|
for (const issue of issues) {
|
|
417
431
|
const assignee = issue.fields.assignee?.displayName ?? 'Unassigned';
|
|
418
|
-
lines.push(` - [${issue.key}] ${issue.fields.summary} | ${issue.fields.status.name} | ${assignee}`);
|
|
432
|
+
lines.push(` - [${issue.key}] ${issue.fields.summary} | ${issue.fields.status.name} | ${assignee} | ${this.issueUrl(issue.key)}`);
|
|
419
433
|
}
|
|
420
434
|
if ((issueData?.total ?? 0) > issues.length) {
|
|
421
435
|
lines.push(` ...and ${(issueData?.total ?? 0) - issues.length} more (adjust issueStartAt/issueMaxResults).`);
|
|
@@ -433,11 +447,12 @@ export class JiraClient {
|
|
|
433
447
|
const data = await this.createIssueInternal(args);
|
|
434
448
|
if (!data)
|
|
435
449
|
return text('Issue created.');
|
|
450
|
+
const url = this.issueUrl(data.key);
|
|
436
451
|
if (args.sprintId !== undefined) {
|
|
437
452
|
await this.addIssuesToSprintInternal(args.sprintId, [data.key]);
|
|
438
|
-
return text(`Created ${data.key} and added it to sprint ${args.sprintId}
|
|
453
|
+
return text(`Created ${data.key} and added it to sprint ${args.sprintId}.\n${url}`);
|
|
439
454
|
}
|
|
440
|
-
return text(`Created ${data.key}
|
|
455
|
+
return text(`Created ${data.key}.\n${url}`);
|
|
441
456
|
}
|
|
442
457
|
async updateIssue(args) {
|
|
443
458
|
const hasFieldUpdates = await this.updateIssueFieldsInternal(args);
|
|
@@ -447,12 +462,12 @@ export class JiraClient {
|
|
|
447
462
|
await this.addIssuesToSprintInternal(args.sprintId, [args.issueKey]);
|
|
448
463
|
}
|
|
449
464
|
if (hasFieldUpdates && args.sprintId !== undefined) {
|
|
450
|
-
return text(`Updated ${args.issueKey} and added it to sprint ${args.sprintId}
|
|
465
|
+
return text(`Updated ${args.issueKey} and added it to sprint ${args.sprintId}.\n${this.issueUrl(args.issueKey)}`);
|
|
451
466
|
}
|
|
452
467
|
if (hasFieldUpdates) {
|
|
453
|
-
return text(`Updated ${args.issueKey}
|
|
468
|
+
return text(`Updated ${args.issueKey}.\n${this.issueUrl(args.issueKey)}`);
|
|
454
469
|
}
|
|
455
|
-
return text(`Added ${args.issueKey} to sprint ${args.sprintId}
|
|
470
|
+
return text(`Added ${args.issueKey} to sprint ${args.sprintId}.\n${this.issueUrl(args.issueKey)}`);
|
|
456
471
|
}
|
|
457
472
|
async addIssuesToSprint(args) {
|
|
458
473
|
const keys = new Set();
|
|
@@ -468,7 +483,14 @@ export class JiraClient {
|
|
|
468
483
|
}
|
|
469
484
|
const issueKeys = Array.from(keys);
|
|
470
485
|
await this.addIssuesToSprintInternal(args.sprintId, issueKeys);
|
|
471
|
-
|
|
486
|
+
if (issueKeys.length === 1) {
|
|
487
|
+
return text(`Added ${issueKeys[0]} to sprint ${args.sprintId}.\n${this.issueUrl(issueKeys[0])}`);
|
|
488
|
+
}
|
|
489
|
+
const lines = [
|
|
490
|
+
`Added ${issueKeys.length} issue(s) to sprint ${args.sprintId}.`,
|
|
491
|
+
...issueKeys.map((issueKey) => `${issueKey}: ${this.issueUrl(issueKey)}`),
|
|
492
|
+
];
|
|
493
|
+
return text(lines.join('\n'));
|
|
472
494
|
}
|
|
473
495
|
async mutateIssue(args) {
|
|
474
496
|
let issueKey = args.issueKey?.trim();
|
|
@@ -504,7 +526,7 @@ export class JiraClient {
|
|
|
504
526
|
if (actions.length === 0) {
|
|
505
527
|
return text('Nothing to mutate.');
|
|
506
528
|
}
|
|
507
|
-
return text(`Mutated ${issueKey}: ${actions.join(', ')}
|
|
529
|
+
return text(`Mutated ${issueKey}: ${actions.join(', ')}.\n${this.issueUrl(issueKey)}`);
|
|
508
530
|
}
|
|
509
531
|
async getComments(args) {
|
|
510
532
|
const { issueKey, maxResults = 50, startAt = 0 } = args;
|
|
@@ -525,6 +547,6 @@ export class JiraClient {
|
|
|
525
547
|
async transitionIssue(args) {
|
|
526
548
|
const transitionId = await this.resolveTransitionId(args.issueKey, args.transitionId, args.transitionName);
|
|
527
549
|
await this.transitionIssueInternal(args.issueKey, transitionId);
|
|
528
|
-
return text(`Transitioned ${args.issueKey} using transition ${transitionId}
|
|
550
|
+
return text(`Transitioned ${args.issueKey} using transition ${transitionId}.\n${this.issueUrl(args.issueKey)}`);
|
|
529
551
|
}
|
|
530
552
|
}
|