@atlassian-dc-mcp/bitbucket 0.11.2 → 0.12.1

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.
@@ -4,6 +4,83 @@ import {
4
4
  type BitbucketPRApiResponse,
5
5
  type SimplifiedPRResponse
6
6
  } from '../pr-comment-mapper.js';
7
+ import { formatToolResponse } from '@atlassian-dc-mcp/common';
8
+
9
+ interface TestComment {
10
+ properties: { repositoryId: number };
11
+ id: number;
12
+ version: number;
13
+ text: string;
14
+ author: ReturnType<typeof createUser>;
15
+ createdDate: number;
16
+ updatedDate: number;
17
+ comments: TestComment[];
18
+ anchor?: {
19
+ fromHash: string;
20
+ toHash: string;
21
+ line: number;
22
+ lineType: string;
23
+ fileType: string;
24
+ path: string;
25
+ diffType: string;
26
+ orphaned: boolean;
27
+ };
28
+ threadResolved: boolean;
29
+ severity: string;
30
+ state: string;
31
+ permittedOperations: {
32
+ editable: boolean;
33
+ transitionable: boolean;
34
+ deletable: boolean;
35
+ };
36
+ }
37
+
38
+ function createUser(name: string, id: number, displayName: string) {
39
+ return {
40
+ name,
41
+ emailAddress: `${name}@company.local`,
42
+ active: true,
43
+ displayName,
44
+ id,
45
+ slug: name,
46
+ type: "NORMAL",
47
+ links: {
48
+ self: [{ href: `https://bitbucket.company.local/users/${name}` }]
49
+ }
50
+ };
51
+ }
52
+
53
+ function createComment(overrides: Partial<TestComment> = {}): TestComment {
54
+ return {
55
+ properties: { repositoryId: 1001 },
56
+ id: 2001,
57
+ version: 0,
58
+ text: "This needs review",
59
+ author: createUser('testuser1', 101, 'User A'),
60
+ createdDate: 1600000000000,
61
+ updatedDate: 1600000000000,
62
+ comments: [],
63
+ anchor: {
64
+ fromHash: "abc123def456789012345678901234567890abcd",
65
+ toHash: "def456abc789012345678901234567890123cdef",
66
+ line: 6,
67
+ lineType: "ADDED",
68
+ fileType: "TO",
69
+ path: "config.yml",
70
+ diffType: "EFFECTIVE",
71
+ orphaned: false
72
+ },
73
+ threadResolved: false,
74
+ severity: "NORMAL",
75
+ state: "OPEN",
76
+ permittedOperations: {
77
+ editable: false,
78
+ transitionable: true,
79
+ deletable: false
80
+ },
81
+ ...overrides
82
+ };
83
+ }
7
84
 
8
85
  describe('PR Comment Mapper', () => {
9
86
  // Test data from the original ad-hoc test
@@ -15,75 +92,15 @@ describe('PR Comment Mapper', () => {
15
92
  {
16
93
  id: 1001,
17
94
  createdDate: 1600000000000,
18
- user: {
19
- name: "testuser1",
20
- emailAddress: "testuser1@company.local",
21
- active: true,
22
- displayName: "User A",
23
- id: 101,
24
- slug: "testuser1",
25
- type: "NORMAL",
26
- links: {
27
- self: [{ href: "https://bitbucket.company.local/users/testuser1" }]
28
- }
29
- },
95
+ user: createUser('testuser1', 101, 'User A'),
30
96
  action: "COMMENTED",
31
97
  commentAction: "ADDED",
32
- comment: {
33
- properties: { repositoryId: 1001 },
34
- id: 2001,
35
- version: 0,
36
- text: "This needs review",
37
- author: {
38
- name: "testuser1",
39
- emailAddress: "testuser1@company.local",
40
- active: true,
41
- displayName: "User A",
42
- id: 101,
43
- slug: "testuser1",
44
- type: "NORMAL",
45
- links: {
46
- self: [{ href: "https://bitbucket.company.local/users/testuser1" }]
47
- }
48
- },
49
- createdDate: 1600000000000,
50
- updatedDate: 1600000000000,
51
- comments: [],
52
- anchor: {
53
- fromHash: "abc123def456789012345678901234567890abcd",
54
- toHash: "def456abc789012345678901234567890123cdef",
55
- line: 6,
56
- lineType: "ADDED",
57
- fileType: "TO",
58
- path: "config.yml",
59
- diffType: "EFFECTIVE",
60
- orphaned: false
61
- },
62
- threadResolved: false,
63
- severity: "NORMAL",
64
- state: "OPEN",
65
- permittedOperations: {
66
- editable: false,
67
- transitionable: true,
68
- deletable: false
69
- }
70
- }
98
+ comment: createComment()
71
99
  },
72
100
  {
73
101
  id: 1002,
74
102
  createdDate: 1600000001000,
75
- user: {
76
- name: "testuser2",
77
- emailAddress: "testuser2@company.local",
78
- active: true,
79
- displayName: "User B",
80
- id: 102,
81
- slug: "testuser2",
82
- type: "NORMAL",
83
- links: {
84
- self: [{ href: "https://bitbucket.company.local/users/testuser2" }]
85
- }
86
- },
103
+ user: createUser('testuser2', 102, 'User B'),
87
104
  action: "OPENED"
88
105
  }
89
106
  ],
@@ -119,6 +136,7 @@ describe('PR Comment Mapper', () => {
119
136
  path: "config.yml",
120
137
  fileType: "TO"
121
138
  },
139
+ comments: [],
122
140
  threadResolved: false,
123
141
  state: "OPEN"
124
142
  }
@@ -147,6 +165,157 @@ describe('PR Comment Mapper', () => {
147
165
  expect(result).toEqual(expectedResult);
148
166
  });
149
167
 
168
+ it('should preserve nested replies recursively', () => {
169
+ const threadedResponse: BitbucketPRApiResponse = {
170
+ ...validPRResponse,
171
+ values: [
172
+ {
173
+ id: 1001,
174
+ createdDate: 1600000000000,
175
+ user: createUser('testuser1', 101, 'User A'),
176
+ action: "COMMENTED",
177
+ commentAction: "ADDED",
178
+ comment: createComment({
179
+ comments: [
180
+ createComment({
181
+ id: 2002,
182
+ text: "Author reply",
183
+ author: createUser('author1', 103, 'Author One'),
184
+ anchor: undefined,
185
+ comments: [
186
+ createComment({
187
+ id: 2003,
188
+ text: "Reviewer follow-up",
189
+ author: createUser('reviewer2', 104, 'Reviewer Two'),
190
+ anchor: undefined
191
+ })
192
+ ]
193
+ })
194
+ ]
195
+ })
196
+ }
197
+ ]
198
+ };
199
+
200
+ const result = simplifyBitbucketPRComments(threadedResponse) as SimplifiedPRResponse;
201
+ expect(result.activities[0].comment?.comments).toEqual([
202
+ {
203
+ id: 2002,
204
+ text: "Author reply",
205
+ author: {
206
+ name: "author1",
207
+ displayName: "Author One"
208
+ },
209
+ createdDate: 1600000000000,
210
+ comments: [
211
+ {
212
+ id: 2003,
213
+ text: "Reviewer follow-up",
214
+ author: {
215
+ name: "reviewer2",
216
+ displayName: "Reviewer Two"
217
+ },
218
+ createdDate: 1600000000000,
219
+ comments: [],
220
+ threadResolved: false,
221
+ state: "OPEN"
222
+ }
223
+ ],
224
+ threadResolved: false,
225
+ state: "OPEN"
226
+ }
227
+ ]);
228
+ });
229
+
230
+ it('should skip malformed nested replies', () => {
231
+ const malformedNestedResponse: BitbucketPRApiResponse = {
232
+ ...validPRResponse,
233
+ values: [
234
+ {
235
+ id: 1001,
236
+ createdDate: 1600000000000,
237
+ user: createUser('testuser1', 101, 'User A'),
238
+ action: "COMMENTED",
239
+ commentAction: "ADDED",
240
+ comment: createComment({
241
+ comments: [
242
+ { id: 'bad-comment-id' } as unknown as TestComment,
243
+ createComment({
244
+ id: 2002,
245
+ text: "Valid reply",
246
+ anchor: undefined
247
+ })
248
+ ]
249
+ })
250
+ }
251
+ ]
252
+ };
253
+
254
+ const result = simplifyBitbucketPRComments(malformedNestedResponse) as SimplifiedPRResponse;
255
+ expect(result.activities[0].comment?.comments).toEqual([
256
+ {
257
+ id: 2002,
258
+ text: "Valid reply",
259
+ author: {
260
+ name: "testuser1",
261
+ displayName: "User A"
262
+ },
263
+ createdDate: 1600000000000,
264
+ comments: [],
265
+ threadResolved: false,
266
+ state: "OPEN"
267
+ }
268
+ ]);
269
+ });
270
+
271
+ it('should drop cyclic reply branches and remain JSON serializable', () => {
272
+ const rootComment = createComment();
273
+ const childComment = createComment({
274
+ id: 2002,
275
+ text: "Child reply",
276
+ anchor: undefined
277
+ });
278
+
279
+ rootComment.comments = [childComment];
280
+ childComment.comments = [rootComment];
281
+
282
+ const cyclicResponse: BitbucketPRApiResponse = {
283
+ size: 1,
284
+ limit: 100,
285
+ isLastPage: true,
286
+ values: [
287
+ {
288
+ id: 1001,
289
+ createdDate: 1600000000000,
290
+ user: createUser('testuser1', 101, 'User A'),
291
+ action: "COMMENTED",
292
+ commentAction: "ADDED",
293
+ comment: rootComment
294
+ }
295
+ ],
296
+ start: 0
297
+ };
298
+
299
+ const result = simplifyBitbucketPRComments(cyclicResponse) as SimplifiedPRResponse;
300
+ expect(result.activities[0].comment?.comments).toEqual([
301
+ {
302
+ id: 2002,
303
+ text: "Child reply",
304
+ author: {
305
+ name: "testuser1",
306
+ displayName: "User A"
307
+ },
308
+ createdDate: 1600000000000,
309
+ comments: [],
310
+ threadResolved: false,
311
+ state: "OPEN"
312
+ }
313
+ ]);
314
+
315
+ expect(() => formatToolResponse(result)).not.toThrow();
316
+ expect(JSON.parse(formatToolResponse(result).content[0].text)).toBeTruthy();
317
+ });
318
+
150
319
  it('should handle malformed input gracefully', () => {
151
320
  const malformedInput = { invalid: 'data' } as any;
152
321
  const result = simplifyBitbucketPRComments(malformedInput) as SimplifiedPRResponse;
@@ -570,6 +570,44 @@ export class BitbucketService {
570
570
  );
571
571
  }
572
572
 
573
+ /**
574
+ * Get pull requests from the dashboard API (across all repositories)
575
+ * @param role Role filter: AUTHOR (default), REVIEWER, or PARTICIPANT
576
+ * @param state State filter: OPEN (default), DECLINED, or MERGED
577
+ * @param closedSince Optional timestamp (in milliseconds) to filter PRs closed after this date
578
+ * @param order Order: NEWEST (default), OLDEST, or PARTICIPANT
579
+ * @param start Optional pagination start
580
+ * @param limit Pagination limit (default: 10)
581
+ * @returns Promise with dashboard pull requests data
582
+ */
583
+ async getDashboardPullRequests(
584
+ role: string = 'AUTHOR',
585
+ state: string = 'OPEN',
586
+ closedSince?: number,
587
+ order: string = 'NEWEST',
588
+ start?: number,
589
+ limit: number = 10
590
+ ) {
591
+ return handleApiOperation(
592
+ () => __request(OpenAPI, {
593
+ method: 'GET',
594
+ url: '/api/1.0/dashboard/pull-requests',
595
+ query: {
596
+ 'role': role,
597
+ 'state': state,
598
+ 'closedSince': closedSince,
599
+ 'order': order,
600
+ 'start': start,
601
+ 'limit': limit,
602
+ },
603
+ errors: {
604
+ 401: 'The currently authenticated user is not permitted to access the dashboard.',
605
+ },
606
+ }),
607
+ 'Error fetching dashboard pull requests'
608
+ );
609
+ }
610
+
573
611
  /**
574
612
  * Get pull requests from the authenticated user's inbox (PRs awaiting review)
575
613
  * @param start Optional pagination start
@@ -741,6 +779,14 @@ export const bitbucketToolSchemas = {
741
779
  sourceRepoId: z.string().optional().describe("Optional ID of the repository in which the source ref exists"),
742
780
  targetRepoId: z.string().optional().describe("Optional ID of the repository in which the target ref exists")
743
781
  },
782
+ getDashboardPullRequests: {
783
+ role: z.enum(['AUTHOR', 'REVIEWER', 'PARTICIPANT']).optional().default('AUTHOR').describe("Filter by the user's role in the PR: AUTHOR (default), REVIEWER, or PARTICIPANT"),
784
+ state: z.enum(['OPEN', 'DECLINED', 'MERGED']).optional().default('OPEN').describe("Filter by PR state: OPEN (default), DECLINED, or MERGED"),
785
+ closedSince: z.number().optional().describe("Timestamp in milliseconds. If state is not OPEN, return only PRs closed after this date"),
786
+ order: z.enum(['NEWEST', 'OLDEST', 'PARTICIPANT']).optional().default('NEWEST').describe("Order of results: NEWEST (default), OLDEST, or PARTICIPANT"),
787
+ start: z.number().optional().describe("Start number for pagination"),
788
+ limit: z.number().optional().default(10).describe("Number of items to return (default: 10)")
789
+ },
744
790
  getInboxPullRequests: {
745
791
  start: z.number().optional().describe("Start number for the page (inclusive). If not passed, first page is assumed"),
746
792
  limit: z.number().optional().default(25).describe("Number of items to return. If not passed, a page size of 25 is used")
package/src/index.ts CHANGED
@@ -193,4 +193,14 @@ server.tool(
193
193
  }
194
194
  );
195
195
 
196
+ server.tool(
197
+ "bitbucket_getDashboardPullRequests",
198
+ "Get pull requests from the Bitbucket dashboard across all repositories. Useful for finding all PRs where you are the author, reviewer, or participant without specifying a project or repository.",
199
+ bitbucketToolSchemas.getDashboardPullRequests,
200
+ async ({ role, state, closedSince, order, start, limit }) => {
201
+ const result = await bitbucketService.getDashboardPullRequests(role, state, closedSince, order, start, limit);
202
+ return formatToolResponse(result);
203
+ }
204
+ );
205
+
196
206
  await connectServer(server);
@@ -139,6 +139,7 @@ interface SimplifiedComment {
139
139
  author: SimplifiedUser;
140
140
  createdDate: number;
141
141
  anchor?: SimplifiedAnchor;
142
+ comments: SimplifiedComment[];
142
143
  threadResolved: boolean;
143
144
  state: string;
144
145
  }
@@ -239,13 +240,20 @@ function simplifyAnchor(anchor: CommentAnchor): SimplifiedAnchor {
239
240
  };
240
241
  }
241
242
 
242
- function simplifyComment(comment: Comment): SimplifiedComment {
243
+ function simplifyComment(comment: Comment, ancestorIds: Set<number> = new Set()): SimplifiedComment {
244
+ const nextAncestorIds = new Set(ancestorIds);
245
+ nextAncestorIds.add(comment.id);
246
+
243
247
  return {
244
248
  id: comment.id,
245
249
  text: comment.text,
246
250
  author: simplifyUser(comment.author),
247
251
  createdDate: comment.createdDate,
248
252
  ...(comment.anchor && { anchor: simplifyAnchor(comment.anchor) }),
253
+ comments: comment.comments
254
+ .filter(isComment)
255
+ .filter(childComment => !nextAncestorIds.has(childComment.id))
256
+ .map(childComment => simplifyComment(childComment, nextAncestorIds)),
249
257
  threadResolved: comment.threadResolved,
250
258
  state: comment.state
251
259
  };
@@ -258,7 +266,9 @@ function simplifyActivity(activity: PRActivity): SimplifiedActivity {
258
266
  user: simplifyUser(activity.user),
259
267
  action: activity.action,
260
268
  ...(activity.commentAction && { commentAction: activity.commentAction }),
261
- ...(activity.comment && { comment: simplifyComment(activity.comment) })
269
+ ...(activity.comment && isComment(activity.comment) && {
270
+ comment: simplifyComment(activity.comment)
271
+ })
262
272
  };
263
273
  }
264
274