@stubbedev/atlassian-mcp 0.1.5 → 0.1.6

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/dist/bitbucket.js CHANGED
@@ -158,6 +158,13 @@ export class BitbucketClient {
158
158
  Accept: 'application/json',
159
159
  };
160
160
  }
161
+ pullRequestUrl(projectKey, repoSlug, prId, pr) {
162
+ const apiUrl = pr?.links?.self?.[0]?.href?.trim();
163
+ if (apiUrl) {
164
+ return apiUrl;
165
+ }
166
+ return `${this.baseUrl}/projects/${encodeURIComponent(projectKey)}/repos/${encodeURIComponent(repoSlug)}/pull-requests/${prId}`;
167
+ }
161
168
  resolveProjectAndRepo(projectKey, repoSlug) {
162
169
  if (projectKey && repoSlug)
163
170
  return { projectKey, repoSlug };
@@ -375,20 +382,21 @@ export class BitbucketClient {
375
382
  const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests`, body);
376
383
  if (!data)
377
384
  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}` : ''}`);
385
+ const url = this.pullRequestUrl(projectKey, repoSlug, data.id, data);
386
+ return text(`Created PR #${data.id}: "${data.title}"\n${url}`);
380
387
  }
381
388
  async approvePr(args) {
382
389
  const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
383
390
  const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/approve`);
391
+ const url = this.pullRequestUrl(projectKey, repoSlug, args.prId);
384
392
  if (!data)
385
- return text(`Approved PR #${args.prId}.`);
386
- return text(`Approved PR #${args.prId} as ${data.user.displayName}.`);
393
+ return text(`Approved PR #${args.prId}.\n${url}`);
394
+ return text(`Approved PR #${args.prId} as ${data.user.displayName}.\n${url}`);
387
395
  }
388
396
  async unapprovePr(args) {
389
397
  const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
390
398
  await this.request('DELETE', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/approve`);
391
- return text(`Approval removed from PR #${args.prId}.`);
399
+ return text(`Approval removed from PR #${args.prId}.\n${this.pullRequestUrl(projectKey, repoSlug, args.prId)}`);
392
400
  }
393
401
  async declinePr(args) {
394
402
  const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
@@ -400,8 +408,8 @@ export class BitbucketClient {
400
408
  body.message = args.message;
401
409
  const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/decline`, body);
402
410
  if (!data)
403
- return text(`Declined PR #${args.prId}.`);
404
- return text(`Declined PR #${data.id}: "${data.title}".`);
411
+ return text(`Declined PR #${args.prId}.\n${this.pullRequestUrl(projectKey, repoSlug, args.prId)}`);
412
+ return text(`Declined PR #${data.id}: "${data.title}".\n${this.pullRequestUrl(projectKey, repoSlug, data.id, data)}`);
405
413
  }
406
414
  async mergePr(args) {
407
415
  const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
@@ -415,8 +423,8 @@ export class BitbucketClient {
415
423
  body.message = args.message;
416
424
  const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/merge`, body);
417
425
  if (!data)
418
- return text(`Merged PR #${args.prId}.`);
419
- return text(`Merged PR #${data.id}: "${data.title}" (${data.fromRef.displayId} → ${data.toRef.displayId}).`);
426
+ return text(`Merged PR #${args.prId}.\n${this.pullRequestUrl(projectKey, repoSlug, args.prId)}`);
427
+ return text(`Merged PR #${data.id}: "${data.title}" (${data.fromRef.displayId} → ${data.toRef.displayId}).\n${this.pullRequestUrl(projectKey, repoSlug, data.id, data)}`);
420
428
  }
421
429
  async getBranches(args) {
422
430
  const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
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.6",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",