@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 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, state, or severity (`BLOCKER` = task/checklist item) |
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
- if (activity.action === 'COMMENTED' && activity.comment && !byId.has(activity.comment.id)) {
71
- byId.set(activity.comment.id, activity.comment);
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()).sort((a, b) => (a.createdDate ?? 0) - (b.createdDate ?? 0));
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.links?.self?.[0]?.href ?? '';
379
- return text(`Created PR #${data.id}: "${data.title}"${url ? `\n${url}` : ''}`);
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 severity');
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 body = {
544
- version: current.version,
545
- text: args.text !== undefined ? validateCommentText(args.text) : current.text,
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
- if (args.state)
548
- body.state = args.state;
549
- if (args.severity)
550
- body.severity = args.severity;
551
- const updated = await this.request('PUT', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}`, body);
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
- return text(`Comment #${updated.id} updated (${state}/${severity}).`);
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: 'Comment state filter (default OPEN; BLOCKER mode supports OPEN/RESOLVED)', default: 'OPEN' },
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 them, or mark comments as task-style BLOCKER items. Keep comments concise, plain text, and free of filler. Never include emojis. You can pass projectKey/repoSlug or project/repo.',
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: 'Comment state (optional)' },
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
- return text(`Added ${issueKeys.length} issue(s) to sprint ${args.sprintId}.`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",