@stubbedev/atlassian-mcp 0.1.16 → 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/README.md +16 -10
- package/dist/bitbucket.js +39 -1
- package/dist/index.js +43 -13
- package/dist/jira.js +64 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -273,10 +273,23 @@ This package is published to npm as `@stubbedev/atlassian-mcp`.
|
|
|
273
273
|
|
|
274
274
|
Use semantic versioning for releases. Breaking tool-surface changes should bump the minor version while `<1.0.0` (for example `0.0.x` -> `0.1.0`).
|
|
275
275
|
|
|
276
|
-
Automatic publish is configured in `.github/workflows/publish.yml
|
|
276
|
+
Automatic publish is configured in `.github/workflows/publish.yml` and runs when a new version tag is pushed.
|
|
277
277
|
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
Release flow:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# choose one: patch | minor | major
|
|
282
|
+
increment=patch
|
|
283
|
+
|
|
284
|
+
# bumps package.json + package-lock.json,
|
|
285
|
+
# creates a version commit, and creates a git tag (for example v0.1.17)
|
|
286
|
+
npm version "$increment"
|
|
287
|
+
|
|
288
|
+
# push commit and tag to GitHub
|
|
289
|
+
git push origin HEAD --follow-tags
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
GitHub Actions will publish the npm release from that pushed tag.
|
|
280
293
|
|
|
281
294
|
- The workflow is configured for npm Trusted Publisher (OIDC), so no `NPM_TOKEN` secret is required
|
|
282
295
|
|
|
@@ -284,13 +297,6 @@ Required npm setup (one-time):
|
|
|
284
297
|
|
|
285
298
|
- In npm package settings, add this GitHub repo/workflow as a Trusted Publisher
|
|
286
299
|
|
|
287
|
-
Manual publish from local machine:
|
|
288
|
-
|
|
289
|
-
```bash
|
|
290
|
-
npm run build
|
|
291
|
-
npm publish --access public
|
|
292
|
-
```
|
|
293
|
-
|
|
294
300
|
---
|
|
295
301
|
|
|
296
302
|
## Creating Personal Access Tokens
|
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
|
-
|
|
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
|
@@ -39,6 +39,7 @@ function normalizeJiraMutateArgs(args) {
|
|
|
39
39
|
}
|
|
40
40
|
return out;
|
|
41
41
|
}
|
|
42
|
+
const JIRA_WIKI_MARKUP_HINT = 'Use Jira wiki markup (Atlassian renderer syntax), not GitHub/CommonMark markdown.';
|
|
42
43
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
43
44
|
tools: [
|
|
44
45
|
// ── Context ───────────────────────────────────────────────────────────
|
|
@@ -157,7 +158,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
157
158
|
},
|
|
158
159
|
{
|
|
159
160
|
name: 'jira_create_issue',
|
|
160
|
-
description:
|
|
161
|
+
description: `Use when you want to create a new Jira ticket (bug, story, task, etc.). If projectKey/project is omitted, the server auto-picks from branch context or asks you to choose. ${JIRA_WIKI_MARKUP_HINT}`,
|
|
161
162
|
inputSchema: {
|
|
162
163
|
type: 'object',
|
|
163
164
|
properties: {
|
|
@@ -165,7 +166,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
165
166
|
project: { type: 'string', description: 'Alias for projectKey' },
|
|
166
167
|
issueType: { type: 'string', description: 'Issue type name, for example "Bug", "Story", or "Task"' },
|
|
167
168
|
summary: { type: 'string', description: 'Issue title' },
|
|
168
|
-
description: { type: 'string', description:
|
|
169
|
+
description: { type: 'string', description: `Issue description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
|
|
169
170
|
assignee: { type: 'string', description: 'Username to assign to (optional)' },
|
|
170
171
|
priority: { type: 'string', description: 'Priority name, e.g. "High" (optional)' },
|
|
171
172
|
sprintId: { type: 'number', description: 'Sprint ID to immediately add the new issue into (optional)' },
|
|
@@ -175,13 +176,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
175
176
|
},
|
|
176
177
|
{
|
|
177
178
|
name: 'jira_update_issue',
|
|
178
|
-
description:
|
|
179
|
+
description: `Use when you want to edit an existing Jira ticket: title, description, assignee, or priority. ${JIRA_WIKI_MARKUP_HINT}`,
|
|
179
180
|
inputSchema: {
|
|
180
181
|
type: 'object',
|
|
181
182
|
properties: {
|
|
182
183
|
issueKey: { type: 'string', description: 'Jira issue key' },
|
|
183
184
|
summary: { type: 'string', description: 'New summary (optional)' },
|
|
184
|
-
description: { type: 'string', description:
|
|
185
|
+
description: { type: 'string', description: `New description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
|
|
185
186
|
assignee: { type: 'string', description: 'New assignee username, or empty string to unassign (optional)' },
|
|
186
187
|
priority: { type: 'string', description: 'New priority name (optional)' },
|
|
187
188
|
sprintId: { type: 'number', description: 'Sprint ID to add this issue into (optional)' },
|
|
@@ -204,7 +205,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
204
205
|
},
|
|
205
206
|
{
|
|
206
207
|
name: 'jira_mutate_issue',
|
|
207
|
-
description:
|
|
208
|
+
description: `Use when you want to bundle Jira mutations in one call: create or target an issue, then optional update, sprint assignment, transition, and comment. ${JIRA_WIKI_MARKUP_HINT}`,
|
|
208
209
|
inputSchema: {
|
|
209
210
|
type: 'object',
|
|
210
211
|
properties: {
|
|
@@ -216,7 +217,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
216
217
|
project: { type: 'string', description: 'Alias for projectKey' },
|
|
217
218
|
issueType: { type: 'string', description: 'Issue type name, e.g. Bug, Story, Task' },
|
|
218
219
|
summary: { type: 'string', description: 'Issue title' },
|
|
219
|
-
description: { type: 'string', description:
|
|
220
|
+
description: { type: 'string', description: `Issue description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
|
|
220
221
|
assignee: { type: 'string', description: 'Username to assign to (optional)' },
|
|
221
222
|
priority: { type: 'string', description: 'Priority name (optional)' },
|
|
222
223
|
},
|
|
@@ -226,7 +227,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
226
227
|
type: 'object',
|
|
227
228
|
properties: {
|
|
228
229
|
summary: { type: 'string', description: 'New summary (optional)' },
|
|
229
|
-
description: { type: 'string', description:
|
|
230
|
+
description: { type: 'string', description: `New description (optional). ${JIRA_WIKI_MARKUP_HINT}` },
|
|
230
231
|
assignee: { type: 'string', description: 'New assignee username, or empty string to unassign (optional)' },
|
|
231
232
|
priority: { type: 'string', description: 'New priority name (optional)' },
|
|
232
233
|
},
|
|
@@ -234,7 +235,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
234
235
|
sprintId: { type: 'number', description: 'Sprint ID to add the issue into (optional)' },
|
|
235
236
|
transitionId: { type: 'string', description: 'Transition ID (optional if transitionName is provided)' },
|
|
236
237
|
transitionName: { type: 'string', description: 'Transition name, e.g. In Progress (optional if transitionId is provided)' },
|
|
237
|
-
comment: { type: 'string', description:
|
|
238
|
+
comment: { type: 'string', description: `Comment to add after other mutations (optional, no emoji). ${JIRA_WIKI_MARKUP_HINT}` },
|
|
238
239
|
},
|
|
239
240
|
},
|
|
240
241
|
},
|
|
@@ -259,7 +260,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
259
260
|
},
|
|
260
261
|
{
|
|
261
262
|
name: 'jira_get_comments',
|
|
262
|
-
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.',
|
|
263
264
|
inputSchema: {
|
|
264
265
|
type: 'object',
|
|
265
266
|
properties: {
|
|
@@ -272,16 +273,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
272
273
|
},
|
|
273
274
|
{
|
|
274
275
|
name: 'jira_add_comment',
|
|
275
|
-
description:
|
|
276
|
+
description: `Use when you want to leave a comment on a Jira ticket. Keep comments concise and free of filler. Never include emojis. ${JIRA_WIKI_MARKUP_HINT}`,
|
|
276
277
|
inputSchema: {
|
|
277
278
|
type: 'object',
|
|
278
279
|
properties: {
|
|
279
280
|
issueKey: { type: 'string', description: 'Jira issue key' },
|
|
280
|
-
body: { type: 'string', description:
|
|
281
|
+
body: { type: 'string', description: `Concise comment text. No filler. Do not include emojis. ${JIRA_WIKI_MARKUP_HINT}` },
|
|
281
282
|
},
|
|
282
283
|
required: ['issueKey', 'body'],
|
|
283
284
|
},
|
|
284
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
|
+
},
|
|
285
311
|
{
|
|
286
312
|
name: 'jira_transition_issue',
|
|
287
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.',
|
|
@@ -604,7 +630,7 @@ Keep comments concise, plain text, and free of filler. Never include emojis. You
|
|
|
604
630
|
},
|
|
605
631
|
{
|
|
606
632
|
name: 'bitbucket_update_pr_comment',
|
|
607
|
-
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.',
|
|
608
634
|
inputSchema: {
|
|
609
635
|
type: 'object',
|
|
610
636
|
properties: {
|
|
@@ -624,7 +650,7 @@ Keep comments concise, plain text, and free of filler. Never include emojis. You
|
|
|
624
650
|
},
|
|
625
651
|
{
|
|
626
652
|
name: 'bitbucket_delete_pr_comment',
|
|
627
|
-
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.',
|
|
628
654
|
inputSchema: {
|
|
629
655
|
type: 'object',
|
|
630
656
|
properties: {
|
|
@@ -746,6 +772,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
746
772
|
return await jira.getComments(args);
|
|
747
773
|
case 'jira_add_comment':
|
|
748
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);
|
|
749
779
|
case 'jira_transition_issue':
|
|
750
780
|
return await jira.transitionIssue(args);
|
|
751
781
|
// Bitbucket
|
package/dist/jira.js
CHANGED
|
@@ -83,13 +83,14 @@ function validateCommentBody(body) {
|
|
|
83
83
|
throw new Error('Jira comment body must not be empty.');
|
|
84
84
|
}
|
|
85
85
|
if (EMOJI_RE.test(trimmed)) {
|
|
86
|
-
throw new Error('Jira comments must not include emoji. Use concise plain text only.');
|
|
86
|
+
throw new Error('Jira comments must not include emoji. Use concise Jira wiki markup or plain text only.');
|
|
87
87
|
}
|
|
88
88
|
return trimmed;
|
|
89
89
|
}
|
|
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);
|