@stubbedev/atlassian-mcp 0.1.17 → 0.1.19
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 +1 -1
- package/dist/bitbucket.js +47 -2
- package/dist/index.js +32 -3
- package/dist/jira.js +63 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
54
54
|
| `bitbucket_decline_pr` | Decline a pull request |
|
|
55
55
|
| `bitbucket_get_pr_comments` | Get PR comment threads in bulk, including task-style BLOCKER comments and blocker counts |
|
|
56
56
|
| `bitbucket_add_pr_comment` | Add a PR comment; when remarking on an existing comment, pass `commentId` so it is posted as a thread reply |
|
|
57
|
-
| `bitbucket_update_pr_comment` | Update comment text/severity, resolve or reopen normal threads via `threadResolved`, and resolve/reopen BLOCKER tasks via `state`
|
|
57
|
+
| `bitbucket_update_pr_comment` | Update comment text/severity (own comments only), resolve or reopen normal threads via `threadResolved`, and resolve/reopen BLOCKER tasks via `state` |
|
|
58
58
|
| `bitbucket_delete_pr_comment` | Delete a PR comment by comment ID |
|
|
59
59
|
| `bitbucket_get_pr_commits` | List commits included in a pull request |
|
|
60
60
|
| `bitbucket_get_branches` | List branches in a repository |
|
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,7 +791,15 @@ 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.`);
|
|
761
|
-
const
|
|
794
|
+
const currentSeverity = current.severity ?? 'NORMAL';
|
|
795
|
+
const severityIsChanging = args.severity !== undefined && args.severity !== currentSeverity;
|
|
796
|
+
const isResolutionOnlyUpdate = (args.state !== undefined || args.threadResolved !== undefined)
|
|
797
|
+
&& args.text === undefined
|
|
798
|
+
&& !severityIsChanging;
|
|
799
|
+
if (!isResolutionOnlyUpdate) {
|
|
800
|
+
await this.assertOwnComment(current);
|
|
801
|
+
}
|
|
802
|
+
const targetSeverity = args.severity ?? currentSeverity;
|
|
762
803
|
if (args.state && targetSeverity !== 'BLOCKER') {
|
|
763
804
|
throw new Error('state is only supported for BLOCKER comments (tasks). Use threadResolved for normal comment threads.');
|
|
764
805
|
}
|
|
@@ -807,7 +848,11 @@ export class BitbucketClient {
|
|
|
807
848
|
const current = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}`);
|
|
808
849
|
if (!current)
|
|
809
850
|
throw new Error(`Comment #${args.commentId} not found.`);
|
|
810
|
-
|
|
851
|
+
await this.assertOwnComment(current);
|
|
852
|
+
const commentPath = current.severity === 'BLOCKER'
|
|
853
|
+
? `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/blocker-comments/${args.commentId}`
|
|
854
|
+
: `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}`;
|
|
855
|
+
const path = `${commentPath}?version=${current.version}`;
|
|
811
856
|
await this.request('DELETE', path);
|
|
812
857
|
return text(`Comment #${args.commentId} deleted from PR #${args.prId}.`);
|
|
813
858
|
}
|
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. Text/severity edits are limited to your own comments, but thread/task resolution via threadResolved/state is allowed for comments from other users. 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
|
|
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);
|