@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.
- package/CHANGELOG.md +22 -0
- package/README.md +12 -0
- package/build/__tests__/bitbucket-service.test.js +92 -0
- package/build/__tests__/bitbucket-service.test.js.map +1 -1
- package/build/__tests__/pr-comment-mapper.test.js +192 -63
- package/build/__tests__/pr-comment-mapper.test.js.map +1 -1
- package/build/bitbucket-service.d.ts +19 -0
- package/build/bitbucket-service.d.ts.map +1 -1
- package/build/bitbucket-service.js +35 -0
- package/build/bitbucket-service.js.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/build/pr-comment-mapper.d.ts +1 -0
- package/build/pr-comment-mapper.d.ts.map +1 -1
- package/build/pr-comment-mapper.js +10 -2
- package/build/pr-comment-mapper.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/bitbucket-service.test.ts +125 -0
- package/src/__tests__/pr-comment-mapper.test.ts +232 -63
- package/src/bitbucket-service.ts +46 -0
- package/src/index.ts +10 -0
- package/src/pr-comment-mapper.ts +12 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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;
|
package/src/bitbucket-service.ts
CHANGED
|
@@ -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);
|
package/src/pr-comment-mapper.ts
CHANGED
|
@@ -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 &&
|
|
269
|
+
...(activity.comment && isComment(activity.comment) && {
|
|
270
|
+
comment: simplifyComment(activity.comment)
|
|
271
|
+
})
|
|
262
272
|
};
|
|
263
273
|
}
|
|
264
274
|
|