@stubbedev/atlassian-mcp 0.1.17 → 0.1.18

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
@@ -181,6 +181,7 @@ function validateCommentText(textValue) {
181
181
  export class BitbucketClient {
182
182
  baseUrl;
183
183
  headers;
184
+ currentUserCache;
184
185
  constructor(baseUrl, token) {
185
186
  this.baseUrl = baseUrl.replace(/\/$/, '');
186
187
  this.headers = {
@@ -237,6 +238,38 @@ export class BitbucketClient {
237
238
  }
238
239
  return res.text();
239
240
  }
241
+ normalizeIdentity(value) {
242
+ return (value ?? '').trim().toLowerCase();
243
+ }
244
+ async getCurrentUser() {
245
+ if (this.currentUserCache)
246
+ return this.currentUserCache;
247
+ const me = await this.request('GET', '/users/~self');
248
+ if (!me) {
249
+ throw new Error('Could not determine current Bitbucket user identity.');
250
+ }
251
+ this.currentUserCache = me;
252
+ return me;
253
+ }
254
+ async assertOwnComment(comment) {
255
+ const me = await this.getCurrentUser();
256
+ const commentAuthorName = this.normalizeIdentity(comment.author?.name);
257
+ const commentAuthorDisplayName = this.normalizeIdentity(comment.author?.displayName);
258
+ const meName = this.normalizeIdentity(me.name);
259
+ const meSlug = this.normalizeIdentity(me.slug);
260
+ const meDisplayName = this.normalizeIdentity(me.displayName);
261
+ const hasStrongCommentIdentity = commentAuthorName.length > 0;
262
+ const hasStrongUserIdentity = meName.length > 0 || meSlug.length > 0;
263
+ const matchesByName = commentAuthorName.length > 0 && (commentAuthorName === meName || commentAuthorName === meSlug);
264
+ const matchesByDisplayNameFallback = !hasStrongCommentIdentity
265
+ && !hasStrongUserIdentity
266
+ && commentAuthorDisplayName.length > 0
267
+ && commentAuthorDisplayName === meDisplayName;
268
+ if (matchesByName || matchesByDisplayNameFallback) {
269
+ return;
270
+ }
271
+ throw new Error(`You can only edit your own Bitbucket comments. Comment #${comment.id} is authored by ${comment.author?.displayName ?? comment.author?.name ?? 'another user'}.`);
272
+ }
240
273
  // Used internally by context tools — finds the open PR for a given source branch
241
274
  async findOpenPrForBranch(projectKey, repoSlug, branch) {
242
275
  const targetBranch = branchDisplayId(branch);
@@ -758,6 +791,7 @@ export class BitbucketClient {
758
791
  const current = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}`);
759
792
  if (!current)
760
793
  throw new Error(`Comment #${args.commentId} not found.`);
794
+ await this.assertOwnComment(current);
761
795
  const targetSeverity = args.severity ?? current.severity ?? 'NORMAL';
762
796
  if (args.state && targetSeverity !== 'BLOCKER') {
763
797
  throw new Error('state is only supported for BLOCKER comments (tasks). Use threadResolved for normal comment threads.');
@@ -807,7 +841,11 @@ export class BitbucketClient {
807
841
  const current = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}`);
808
842
  if (!current)
809
843
  throw new Error(`Comment #${args.commentId} not found.`);
810
- const path = `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}?version=${current.version}`;
844
+ await this.assertOwnComment(current);
845
+ const commentPath = current.severity === 'BLOCKER'
846
+ ? `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/blocker-comments/${args.commentId}`
847
+ : `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}`;
848
+ const path = `${commentPath}?version=${current.version}`;
811
849
  await this.request('DELETE', path);
812
850
  return text(`Comment #${args.commentId} deleted from PR #${args.prId}.`);
813
851
  }
package/dist/index.js CHANGED
@@ -260,7 +260,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
260
260
  },
261
261
  {
262
262
  name: 'jira_get_comments',
263
- description: 'Use when you want the discussion thread on a Jira ticket, with pagination for long threads.',
263
+ description: 'Use when you want the discussion thread on a Jira ticket, with pagination for long threads. Includes comment IDs for follow-up edits.',
264
264
  inputSchema: {
265
265
  type: 'object',
266
266
  properties: {
@@ -283,6 +283,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
283
283
  required: ['issueKey', 'body'],
284
284
  },
285
285
  },
286
+ {
287
+ name: 'jira_edit_comment',
288
+ description: `Use when you want to edit one of your own comments on a Jira ticket. Editing comments from other users is rejected. Never include emojis. ${JIRA_WIKI_MARKUP_HINT}`,
289
+ inputSchema: {
290
+ type: 'object',
291
+ properties: {
292
+ issueKey: { type: 'string', description: 'Jira issue key' },
293
+ commentId: { type: 'string', description: 'Jira comment ID (from jira_get_comments or jira_issue_overview output)' },
294
+ body: { type: 'string', description: `Updated concise comment text. No filler. Do not include emojis. ${JIRA_WIKI_MARKUP_HINT}` },
295
+ },
296
+ required: ['issueKey', 'commentId', 'body'],
297
+ },
298
+ },
299
+ {
300
+ name: 'jira_delete_comment',
301
+ description: 'Use when you want to delete one of your own comments on a Jira ticket by comment ID. Deleting comments from other users is rejected.',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ issueKey: { type: 'string', description: 'Jira issue key' },
306
+ commentId: { type: 'string', description: 'Jira comment ID to delete (from jira_get_comments or jira_issue_overview output)' },
307
+ },
308
+ required: ['issueKey', 'commentId'],
309
+ },
310
+ },
286
311
  {
287
312
  name: 'jira_transition_issue',
288
313
  description: 'Use when you want to move a Jira ticket to another status. Provide a transition name (for example "In Progress") or a transition ID.',
@@ -605,7 +630,7 @@ Keep comments concise, plain text, and free of filler. Never include emojis. You
605
630
  },
606
631
  {
607
632
  name: 'bitbucket_update_pr_comment',
608
- 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.',
633
+ description: 'Use when you want to edit your own PR comments, resolve/reopen normal discussion threads, or manage task-style BLOCKER comments. Editing comments from other users is rejected. 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.',
609
634
  inputSchema: {
610
635
  type: 'object',
611
636
  properties: {
@@ -625,7 +650,7 @@ Keep comments concise, plain text, and free of filler. Never include emojis. You
625
650
  },
626
651
  {
627
652
  name: 'bitbucket_delete_pr_comment',
628
- description: 'Use when you want to delete a PR comment by comment ID. You can pass projectKey/repoSlug or project/repo.',
653
+ description: 'Use when you want to delete one of your own PR comments by comment ID. Deleting comments from other users is rejected. You can pass projectKey/repoSlug or project/repo.',
629
654
  inputSchema: {
630
655
  type: 'object',
631
656
  properties: {
@@ -747,6 +772,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
747
772
  return await jira.getComments(args);
748
773
  case 'jira_add_comment':
749
774
  return await jira.addComment(args);
775
+ case 'jira_edit_comment':
776
+ return await jira.editComment(args);
777
+ case 'jira_delete_comment':
778
+ return await jira.deleteComment(args);
750
779
  case 'jira_transition_issue':
751
780
  return await jira.transitionIssue(args);
752
781
  // Bitbucket
package/dist/jira.js CHANGED
@@ -90,6 +90,7 @@ function validateCommentBody(body) {
90
90
  export class JiraClient {
91
91
  baseUrl;
92
92
  headers;
93
+ currentUserCache;
93
94
  constructor(baseUrl, token) {
94
95
  this.baseUrl = baseUrl.replace(/\/$/, '');
95
96
  this.headers = {
@@ -129,6 +130,40 @@ export class JiraClient {
129
130
  async requestAgile(method, path, body) {
130
131
  return this.requestWithBase('/rest/agile/1.0', method, path, body);
131
132
  }
133
+ normalizeIdentity(value) {
134
+ return (value ?? '').trim().toLowerCase();
135
+ }
136
+ async getCurrentUser() {
137
+ if (this.currentUserCache)
138
+ return this.currentUserCache;
139
+ const me = await this.request('GET', '/myself');
140
+ if (!me) {
141
+ throw new Error('Could not determine current Jira user identity.');
142
+ }
143
+ this.currentUserCache = me;
144
+ return me;
145
+ }
146
+ async assertOwnComment(comment) {
147
+ const me = await this.getCurrentUser();
148
+ const commentAuthorName = this.normalizeIdentity(comment.author.name);
149
+ const commentAuthorKey = this.normalizeIdentity(comment.author.key);
150
+ const commentAuthorDisplayName = this.normalizeIdentity(comment.author.displayName);
151
+ const meName = this.normalizeIdentity(me.name);
152
+ const meKey = this.normalizeIdentity(me.key);
153
+ const meDisplayName = this.normalizeIdentity(me.displayName);
154
+ const hasStrongCommentIdentity = commentAuthorName.length > 0 || commentAuthorKey.length > 0;
155
+ const hasStrongUserIdentity = meName.length > 0 || meKey.length > 0;
156
+ const matchesByNameOrKey = (commentAuthorName.length > 0 && (commentAuthorName === meName || commentAuthorName === meKey))
157
+ || (commentAuthorKey.length > 0 && (commentAuthorKey === meName || commentAuthorKey === meKey));
158
+ const matchesByDisplayNameFallback = !hasStrongCommentIdentity
159
+ && !hasStrongUserIdentity
160
+ && commentAuthorDisplayName.length > 0
161
+ && commentAuthorDisplayName === meDisplayName;
162
+ if (matchesByNameOrKey || matchesByDisplayNameFallback) {
163
+ return;
164
+ }
165
+ throw new Error(`You can only edit your own Jira comments. Comment ${comment.id} is authored by ${comment.author.displayName}.`);
166
+ }
132
167
  async addIssuesToSprintInternal(sprintId, issueKeys) {
133
168
  await this.requestAgile('POST', `/sprint/${sprintId}/issue`, { issues: issueKeys });
134
169
  }
@@ -365,7 +400,7 @@ export class JiraClient {
365
400
  else {
366
401
  for (const c of items) {
367
402
  const date = c.created.slice(0, 10);
368
- lines.push(`--- ${c.author.displayName} (${date}) ---`, c.body, '');
403
+ lines.push(`--- #${c.id} ${c.author.displayName} (${date}) ---`, c.body, '');
369
404
  }
370
405
  }
371
406
  }
@@ -536,7 +571,7 @@ export class JiraClient {
536
571
  return text('No comments found.');
537
572
  const blocks = data.comments.map((c) => {
538
573
  const date = c.created.slice(0, 10);
539
- return `--- ${c.author.displayName} (${date}) ---\n${c.body}`;
574
+ return `--- #${c.id} ${c.author.displayName} (${date}) ---\n${c.body}`;
540
575
  });
541
576
  const page = pagination(data.total, startAt, data.comments.length);
542
577
  return text(`${data.total} comment(s) on ${issueKey}${page}:\n\n${blocks.join('\n\n')}`);
@@ -545,6 +580,32 @@ export class JiraClient {
545
580
  await this.request('POST', `/issue/${args.issueKey}/comment`, { body: validateCommentBody(args.body) });
546
581
  return text(`Comment added to ${args.issueKey}.`);
547
582
  }
583
+ async editComment(args) {
584
+ const commentId = String(args.commentId).trim();
585
+ if (!commentId) {
586
+ throw new Error('commentId is required.');
587
+ }
588
+ const path = `/issue/${args.issueKey}/comment/${commentId}`;
589
+ const current = await this.request('GET', path);
590
+ if (!current)
591
+ throw new Error(`Comment ${commentId} not found on ${args.issueKey}.`);
592
+ await this.assertOwnComment(current);
593
+ await this.request('PUT', path, { body: validateCommentBody(args.body) });
594
+ return text(`Comment ${commentId} updated on ${args.issueKey}.`);
595
+ }
596
+ async deleteComment(args) {
597
+ const commentId = String(args.commentId).trim();
598
+ if (!commentId) {
599
+ throw new Error('commentId is required.');
600
+ }
601
+ const path = `/issue/${args.issueKey}/comment/${commentId}`;
602
+ const current = await this.request('GET', path);
603
+ if (!current)
604
+ throw new Error(`Comment ${commentId} not found on ${args.issueKey}.`);
605
+ await this.assertOwnComment(current);
606
+ await this.request('DELETE', path);
607
+ return text(`Comment ${commentId} deleted from ${args.issueKey}.`);
608
+ }
548
609
  async transitionIssue(args) {
549
610
  const transitionId = await this.resolveTransitionId(args.issueKey, args.transitionId, args.transitionName);
550
611
  await this.transitionIssueInternal(args.issueKey, transitionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
5
  "license": "MIT",
6
6
  "type": "module",