@stubbedev/atlassian-mcp 0.1.6 → 0.1.8

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stubbedev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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)`;
@@ -542,26 +570,55 @@ export class BitbucketClient {
542
570
  }
543
571
  async updatePrComment(args) {
544
572
  const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
545
- if (!args.text && !args.state && !args.severity) {
546
- 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');
547
575
  }
548
576
  const current = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments/${args.commentId}`);
549
577
  if (!current)
550
578
  throw new Error(`Comment #${args.commentId} not found.`);
551
- const body = {
552
- version: current.version,
553
- 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;
554
601
  };
555
- if (args.state)
556
- body.state = args.state;
557
- if (args.severity)
558
- body.severity = args.severity;
559
- 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
+ }
560
615
  if (!updated)
561
616
  return text(`Comment #${args.commentId} updated.`);
562
617
  const state = updated.state ?? current.state ?? 'OPEN';
563
618
  const severity = updated.severity ?? current.severity ?? 'NORMAL';
564
- 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}).`);
565
622
  }
566
623
  async deletePrComment(args) {
567
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/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",
7
8
  "files": [