@atlassian-dc-mcp/bitbucket 0.10.0 → 0.10.2

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.
@@ -0,0 +1,347 @@
1
+ import { simplifyInboxPullRequests, SimplifiedInboxPRResponse } from '../inbox-pr-mapper.js';
2
+
3
+ describe('simplifyInboxPullRequests', () => {
4
+ const makePR = (overrides: Record<string, unknown> = {}) => ({
5
+ id: 101,
6
+ version: 0,
7
+ title: 'feat: Add user authentication module',
8
+ description: 'Implements OAuth2 login flow',
9
+ state: 'OPEN',
10
+ open: true,
11
+ closed: false,
12
+ draft: false,
13
+ createdDate: 1700000000000,
14
+ updatedDate: 1700001000000,
15
+ locked: false,
16
+ author: {
17
+ user: {
18
+ name: 'jsmith',
19
+ emailAddress: 'jsmith@example.com',
20
+ active: true,
21
+ displayName: 'John Smith',
22
+ id: 1001,
23
+ slug: 'jsmith',
24
+ type: 'NORMAL',
25
+ links: { self: [{ href: 'https://bitbucket.example.com/users/jsmith' }] },
26
+ },
27
+ role: 'AUTHOR',
28
+ approved: false,
29
+ status: 'UNAPPROVED',
30
+ },
31
+ fromRef: {
32
+ id: 'refs/heads/feat/user-auth',
33
+ displayId: 'feat/user-auth',
34
+ latestCommit: 'abc123def456abc123def456abc123def456abc1',
35
+ type: 'BRANCH',
36
+ repository: {
37
+ slug: 'myproject-api',
38
+ id: 100,
39
+ name: 'myproject-api',
40
+ hierarchyId: 'aabb11cc22dd33',
41
+ scmId: 'git',
42
+ state: 'AVAILABLE',
43
+ statusMessage: 'Available',
44
+ forkable: true,
45
+ project: {
46
+ key: 'PROJ',
47
+ id: 10,
48
+ name: 'Project Alpha',
49
+ public: false,
50
+ type: 'NORMAL',
51
+ links: { self: [{ href: 'https://bitbucket.example.com/projects/PROJ' }] },
52
+ },
53
+ public: false,
54
+ archived: false,
55
+ links: {
56
+ clone: [
57
+ { href: 'ssh://git@bitbucket.example.com/proj/myproject-api.git', name: 'ssh' },
58
+ { href: 'https://bitbucket.example.com/scm/proj/myproject-api.git', name: 'http' },
59
+ ],
60
+ self: [{ href: 'https://bitbucket.example.com/projects/PROJ/repos/myproject-api/browse' }],
61
+ },
62
+ },
63
+ },
64
+ toRef: {
65
+ id: 'refs/heads/main',
66
+ displayId: 'main',
67
+ latestCommit: 'def456abc123def456abc123def456abc123def4',
68
+ type: 'BRANCH',
69
+ repository: {
70
+ slug: 'myproject-api',
71
+ id: 100,
72
+ name: 'myproject-api',
73
+ project: { key: 'PROJ', id: 10, name: 'Project Alpha' },
74
+ },
75
+ },
76
+ reviewers: [
77
+ {
78
+ user: { name: 'adoe', displayName: 'Alice Doe', emailAddress: 'adoe@example.com', id: 1002 },
79
+ role: 'REVIEWER',
80
+ approved: false,
81
+ status: 'UNAPPROVED',
82
+ },
83
+ {
84
+ user: { name: 'bjones', displayName: 'Bob Jones', emailAddress: 'bjones@example.com', id: 1003 },
85
+ lastReviewedCommit: 'abc123def456abc123def456abc123def456abc1',
86
+ role: 'REVIEWER',
87
+ approved: true,
88
+ status: 'APPROVED',
89
+ },
90
+ {
91
+ user: { name: 'clee', displayName: 'Carol Lee', emailAddress: 'clee@example.com', id: 1004 },
92
+ role: 'REVIEWER',
93
+ approved: false,
94
+ status: 'UNAPPROVED',
95
+ },
96
+ ],
97
+ participants: [],
98
+ properties: {
99
+ mergeResult: { outcome: 'CLEAN', current: true },
100
+ resolvedTaskCount: 0,
101
+ commentCount: 3,
102
+ openTaskCount: 1,
103
+ },
104
+ links: {
105
+ self: [{ href: 'https://bitbucket.example.com/projects/PROJ/repos/myproject-api/pull-requests/101' }],
106
+ },
107
+ ...overrides,
108
+ });
109
+
110
+ const makeResponse = (prs: unknown[] = [makePR()], overrides: Record<string, unknown> = {}) => ({
111
+ values: prs,
112
+ size: prs.length,
113
+ isLastPage: true,
114
+ start: 0,
115
+ limit: 25,
116
+ ...overrides,
117
+ });
118
+
119
+ it('should simplify a valid inbox response with real API structure', () => {
120
+ const response = makeResponse();
121
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
122
+
123
+ expect(result.pullRequests).toHaveLength(1);
124
+ expect(result.pullRequests[0]).toEqual({
125
+ id: 101,
126
+ title: 'feat: Add user authentication module',
127
+ description: 'Implements OAuth2 login flow',
128
+ state: 'OPEN',
129
+ draft: false,
130
+ createdDate: 1700000000000,
131
+ updatedDate: 1700001000000,
132
+ link: 'https://bitbucket.example.com/projects/PROJ/repos/myproject-api/pull-requests/101',
133
+ author: { name: 'jsmith', displayName: 'John Smith' },
134
+ fromRef: 'feat/user-auth',
135
+ toRef: 'main',
136
+ repository: { slug: 'myproject-api', projectKey: 'PROJ' },
137
+ reviewers: [
138
+ { name: 'adoe', approved: false, status: 'UNAPPROVED' },
139
+ { name: 'bjones', approved: true, status: 'APPROVED' },
140
+ { name: 'clee', approved: false, status: 'UNAPPROVED' },
141
+ ],
142
+ commentCount: 3,
143
+ openTaskCount: 1,
144
+ });
145
+ expect(result.summary).toEqual({
146
+ totalCount: 1,
147
+ byRepository: { 'PROJ/myproject-api': 1 },
148
+ });
149
+ expect(result.isLastPage).toBe(true);
150
+ });
151
+
152
+ it('should extract the PR link from links.self[0].href', () => {
153
+ const response = makeResponse();
154
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
155
+
156
+ expect(result.pullRequests[0].link).toBe(
157
+ 'https://bitbucket.example.com/projects/PROJ/repos/myproject-api/pull-requests/101'
158
+ );
159
+ });
160
+
161
+ it('should omit link when links.self is not present', () => {
162
+ const pr = makePR({ links: undefined });
163
+ const response = makeResponse([pr]);
164
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
165
+
166
+ expect(result.pullRequests[0].link).toBeUndefined();
167
+ });
168
+
169
+ it('should include commentCount and openTaskCount from properties', () => {
170
+ const pr = makePR({
171
+ properties: { commentCount: 5, openTaskCount: 2, resolvedTaskCount: 1 },
172
+ });
173
+ const response = makeResponse([pr]);
174
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
175
+
176
+ expect(result.pullRequests[0].commentCount).toBe(5);
177
+ expect(result.pullRequests[0].openTaskCount).toBe(2);
178
+ });
179
+
180
+ it('should default commentCount and openTaskCount to 0 when properties missing', () => {
181
+ const pr = makePR({ properties: undefined });
182
+ const response = makeResponse([pr]);
183
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
184
+
185
+ expect(result.pullRequests[0].commentCount).toBe(0);
186
+ expect(result.pullRequests[0].openTaskCount).toBe(0);
187
+ });
188
+
189
+ it('should include description when present', () => {
190
+ const response = makeResponse();
191
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
192
+
193
+ expect(result.pullRequests[0].description).toBe('Implements OAuth2 login flow');
194
+ });
195
+
196
+ it('should omit description when not present', () => {
197
+ const pr = makePR({ description: undefined });
198
+ const response = makeResponse([pr]);
199
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
200
+
201
+ expect(result.pullRequests[0].description).toBeUndefined();
202
+ });
203
+
204
+ it('should include draft status', () => {
205
+ const pr = makePR({ draft: true });
206
+ const response = makeResponse([pr]);
207
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
208
+
209
+ expect(result.pullRequests[0].draft).toBe(true);
210
+ });
211
+
212
+ it('should default draft to false when not present', () => {
213
+ const pr = makePR({ draft: undefined });
214
+ const response = makeResponse([pr]);
215
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
216
+
217
+ expect(result.pullRequests[0].draft).toBe(false);
218
+ });
219
+
220
+ it('should handle multiple PRs from different repositories', () => {
221
+ const pr1 = makePR();
222
+ const pr2 = makePR({
223
+ id: 200,
224
+ title: 'fix: Resolve cache invalidation bug',
225
+ fromRef: {
226
+ id: 'refs/heads/fix/cache-bug',
227
+ displayId: 'fix/cache-bug',
228
+ repository: { slug: 'frontend-app', project: { key: 'WEB' } },
229
+ },
230
+ toRef: {
231
+ id: 'refs/heads/main',
232
+ displayId: 'main',
233
+ repository: { slug: 'frontend-app', project: { key: 'WEB' } },
234
+ },
235
+ links: {
236
+ self: [{ href: 'https://bitbucket.example.com/projects/WEB/repos/frontend-app/pull-requests/200' }],
237
+ },
238
+ });
239
+ const pr3 = makePR({ id: 102, title: 'chore: Update dependencies' });
240
+
241
+ const response = makeResponse([pr1, pr2, pr3]);
242
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
243
+
244
+ expect(result.pullRequests).toHaveLength(3);
245
+ expect(result.summary.totalCount).toBe(3);
246
+ expect(result.summary.byRepository).toEqual({
247
+ 'PROJ/myproject-api': 2,
248
+ 'WEB/frontend-app': 1,
249
+ });
250
+ });
251
+
252
+ it('should handle empty values array', () => {
253
+ const response = makeResponse([]);
254
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
255
+
256
+ expect(result.pullRequests).toHaveLength(0);
257
+ expect(result.summary.totalCount).toBe(0);
258
+ expect(result.summary.byRepository).toEqual({});
259
+ expect(result.isLastPage).toBe(true);
260
+ });
261
+
262
+ it('should include nextPageStart when not the last page', () => {
263
+ const response = makeResponse([makePR()], {
264
+ isLastPage: false,
265
+ nextPageStart: 25,
266
+ });
267
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
268
+
269
+ expect(result.isLastPage).toBe(false);
270
+ expect(result.nextPageStart).toBe(25);
271
+ });
272
+
273
+ it('should not include nextPageStart when it is the last page', () => {
274
+ const response = makeResponse([makePR()]);
275
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
276
+
277
+ expect(result.isLastPage).toBe(true);
278
+ expect(result.nextPageStart).toBeUndefined();
279
+ });
280
+
281
+ it('should handle PR without author', () => {
282
+ const pr = makePR({ author: undefined });
283
+ const response = makeResponse([pr]);
284
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
285
+
286
+ expect(result.pullRequests[0].author).toBeUndefined();
287
+ });
288
+
289
+ it('should handle PR without reviewers', () => {
290
+ const pr = makePR({ reviewers: undefined });
291
+ const response = makeResponse([pr]);
292
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
293
+
294
+ expect(result.pullRequests[0].reviewers).toEqual([]);
295
+ });
296
+
297
+ it('should return original response for invalid input', () => {
298
+ const invalidResponse = { foo: 'bar' };
299
+ const result = simplifyInboxPullRequests(invalidResponse);
300
+
301
+ expect(result).toBe(invalidResponse);
302
+ });
303
+
304
+ it('should return original response for null input', () => {
305
+ const result = simplifyInboxPullRequests(null);
306
+ expect(result).toBeNull();
307
+ });
308
+
309
+ it('should return original response when values contain no valid PRs', () => {
310
+ const response = makeResponse([{ invalid: true }, { alsoInvalid: true }]);
311
+ const result = simplifyInboxPullRequests(response);
312
+
313
+ expect(result).toBe(response);
314
+ });
315
+
316
+ it('should use ref id as fallback when displayId is missing', () => {
317
+ const pr = makePR({
318
+ fromRef: {
319
+ id: 'refs/heads/feat/user-auth',
320
+ repository: { slug: 'myproject-api', project: { key: 'PROJ' } },
321
+ },
322
+ toRef: {
323
+ id: 'refs/heads/main',
324
+ repository: { slug: 'myproject-api', project: { key: 'PROJ' } },
325
+ },
326
+ });
327
+ const response = makeResponse([pr]);
328
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
329
+
330
+ expect(result.pullRequests[0].fromRef).toBe('refs/heads/feat/user-auth');
331
+ expect(result.pullRequests[0].toRef).toBe('refs/heads/main');
332
+ });
333
+
334
+ it('should strip extra fields from the real API response', () => {
335
+ const response = makeResponse();
336
+ const result = simplifyInboxPullRequests(response) as SimplifiedInboxPRResponse;
337
+ const pr = result.pullRequests[0];
338
+
339
+ expect(pr).not.toHaveProperty('version');
340
+ expect(pr).not.toHaveProperty('open');
341
+ expect(pr).not.toHaveProperty('closed');
342
+ expect(pr).not.toHaveProperty('locked');
343
+ expect(pr).not.toHaveProperty('participants');
344
+ expect(pr).not.toHaveProperty('properties');
345
+ expect(pr).not.toHaveProperty('links');
346
+ });
347
+ });
@@ -4,6 +4,7 @@ import { request as __request } from './bitbucket-client/core/request.js';
4
4
  import { handleApiOperation } from '@atlassian-dc-mcp/common';
5
5
  import { simplifyBitbucketPRComments } from './pr-comment-mapper.js';
6
6
  import { simplifyBitbucketPRChanges } from './pr-changes-mapper.js';
7
+ import { simplifyInboxPullRequests } from './inbox-pr-mapper.js';
7
8
 
8
9
  export class BitbucketService {
9
10
  constructor(host: string, token: string, fullBaseUrl?: string) {
@@ -496,6 +497,38 @@ export class BitbucketService {
496
497
  );
497
498
  }
498
499
 
500
+ /**
501
+ * Get pull requests from the authenticated user's inbox (PRs awaiting review)
502
+ * @param start Optional pagination start
503
+ * @param limit Optional pagination limit (default: 25)
504
+ * @returns Promise with inbox pull requests data
505
+ */
506
+ async getInboxPullRequests(start?: number, limit: number = 25) {
507
+ const result = await handleApiOperation(
508
+ () => __request(OpenAPI, {
509
+ method: 'GET',
510
+ url: '/api/latest/inbox/pull-requests',
511
+ query: {
512
+ 'start': start,
513
+ 'limit': limit,
514
+ },
515
+ errors: {
516
+ 401: 'The currently authenticated user is not permitted to access the inbox.',
517
+ },
518
+ }),
519
+ 'Error fetching inbox pull requests'
520
+ );
521
+
522
+ if (result.success && result.data) {
523
+ return {
524
+ success: true,
525
+ data: simplifyInboxPullRequests(result.data),
526
+ };
527
+ }
528
+
529
+ return result;
530
+ }
531
+
499
532
  static validateConfig(): string[] {
500
533
  // Check for BITBUCKET_HOST or its alternative BITBUCKET_API_BASE_PATH
501
534
  const requiredEnvVars = ['BITBUCKET_API_TOKEN'] as const;
@@ -621,5 +654,9 @@ export const bitbucketToolSchemas = {
621
654
  targetRefId: z.string().describe("The ID of the target ref (e.g., 'refs/heads/main')"),
622
655
  sourceRepoId: z.string().optional().describe("Optional ID of the repository in which the source ref exists"),
623
656
  targetRepoId: z.string().optional().describe("Optional ID of the repository in which the target ref exists")
657
+ },
658
+ getInboxPullRequests: {
659
+ start: z.number().optional().describe("Start number for the page (inclusive). If not passed, first page is assumed"),
660
+ limit: z.number().optional().default(25).describe("Number of items to return. If not passed, a page size of 25 is used")
624
661
  }
625
662
  };
@@ -0,0 +1,183 @@
1
+ interface InboxPRUser {
2
+ name: string;
3
+ emailAddress?: string;
4
+ displayName?: string;
5
+ }
6
+
7
+ interface InboxPRRef {
8
+ id: string;
9
+ displayId: string;
10
+ latestCommit?: string;
11
+ repository?: {
12
+ slug: string;
13
+ project?: {
14
+ key: string;
15
+ };
16
+ };
17
+ }
18
+
19
+ interface InboxPRReviewer {
20
+ user: InboxPRUser;
21
+ approved: boolean;
22
+ status: string;
23
+ }
24
+
25
+ interface InboxPullRequest {
26
+ id: number;
27
+ title: string;
28
+ description?: string;
29
+ state: string;
30
+ draft?: boolean;
31
+ createdDate: number;
32
+ updatedDate: number;
33
+ author?: {
34
+ user: InboxPRUser;
35
+ };
36
+ fromRef: InboxPRRef;
37
+ toRef: InboxPRRef;
38
+ reviewers?: InboxPRReviewer[];
39
+ properties?: {
40
+ commentCount?: number;
41
+ openTaskCount?: number;
42
+ };
43
+ links?: {
44
+ self?: { href: string }[];
45
+ };
46
+ }
47
+
48
+ interface InboxPRResponse {
49
+ values: InboxPullRequest[];
50
+ size: number;
51
+ isLastPage: boolean;
52
+ start: number;
53
+ limit: number;
54
+ nextPageStart?: number;
55
+ }
56
+
57
+ // Simplified interfaces
58
+ interface SimplifiedInboxPR {
59
+ id: number;
60
+ title: string;
61
+ description?: string;
62
+ state: string;
63
+ draft: boolean;
64
+ createdDate: number;
65
+ updatedDate: number;
66
+ link?: string;
67
+ author?: {
68
+ name: string;
69
+ displayName?: string;
70
+ };
71
+ fromRef: string;
72
+ toRef: string;
73
+ repository: {
74
+ slug: string;
75
+ projectKey: string;
76
+ };
77
+ reviewers: {
78
+ name: string;
79
+ approved: boolean;
80
+ status: string;
81
+ }[];
82
+ commentCount: number;
83
+ openTaskCount: number;
84
+ }
85
+
86
+ export interface SimplifiedInboxPRResponse {
87
+ pullRequests: SimplifiedInboxPR[];
88
+ summary: {
89
+ totalCount: number;
90
+ byRepository: Record<string, number>;
91
+ };
92
+ isLastPage: boolean;
93
+ nextPageStart?: number;
94
+ }
95
+
96
+ // Type guards
97
+ function isInboxPRResponse(obj: unknown): obj is InboxPRResponse {
98
+ return (
99
+ typeof obj === 'object' &&
100
+ obj !== null &&
101
+ Array.isArray((obj as InboxPRResponse).values) &&
102
+ typeof (obj as InboxPRResponse).isLastPage === 'boolean'
103
+ );
104
+ }
105
+
106
+ function isInboxPullRequest(obj: unknown): obj is InboxPullRequest {
107
+ return (
108
+ typeof obj === 'object' &&
109
+ obj !== null &&
110
+ typeof (obj as InboxPullRequest).id === 'number' &&
111
+ typeof (obj as InboxPullRequest).title === 'string' &&
112
+ typeof (obj as InboxPullRequest).state === 'string'
113
+ );
114
+ }
115
+
116
+ function simplifyInboxPR(pr: InboxPullRequest): SimplifiedInboxPR {
117
+ const repo = pr.toRef?.repository ?? pr.fromRef?.repository;
118
+ const link = pr.links?.self?.[0]?.href;
119
+
120
+ return {
121
+ id: pr.id,
122
+ title: pr.title,
123
+ ...(pr.description !== undefined && { description: pr.description }),
124
+ state: pr.state,
125
+ draft: pr.draft ?? false,
126
+ createdDate: pr.createdDate,
127
+ updatedDate: pr.updatedDate,
128
+ ...(link && { link }),
129
+ ...(pr.author?.user && {
130
+ author: {
131
+ name: pr.author.user.name,
132
+ ...(pr.author.user.displayName && { displayName: pr.author.user.displayName }),
133
+ },
134
+ }),
135
+ fromRef: pr.fromRef?.displayId ?? pr.fromRef?.id,
136
+ toRef: pr.toRef?.displayId ?? pr.toRef?.id,
137
+ repository: {
138
+ slug: repo?.slug ?? 'unknown',
139
+ projectKey: repo?.project?.key ?? 'unknown',
140
+ },
141
+ reviewers: (pr.reviewers ?? []).map(r => ({
142
+ name: r.user.name,
143
+ approved: r.approved,
144
+ status: r.status,
145
+ })),
146
+ commentCount: pr.properties?.commentCount ?? 0,
147
+ openTaskCount: pr.properties?.openTaskCount ?? 0,
148
+ };
149
+ }
150
+
151
+ export function simplifyInboxPullRequests(response: unknown): SimplifiedInboxPRResponse | unknown {
152
+ if (!isInboxPRResponse(response)) {
153
+ return response;
154
+ }
155
+
156
+ const pullRequests: SimplifiedInboxPR[] = [];
157
+
158
+ for (const pr of response.values) {
159
+ if (isInboxPullRequest(pr)) {
160
+ pullRequests.push(simplifyInboxPR(pr));
161
+ }
162
+ }
163
+
164
+ if (pullRequests.length === 0 && response.values.length > 0) {
165
+ return response;
166
+ }
167
+
168
+ const byRepository: Record<string, number> = {};
169
+ for (const pr of pullRequests) {
170
+ const repoKey = `${pr.repository.projectKey}/${pr.repository.slug}`;
171
+ byRepository[repoKey] = (byRepository[repoKey] ?? 0) + 1;
172
+ }
173
+
174
+ return {
175
+ pullRequests,
176
+ summary: {
177
+ totalCount: pullRequests.length,
178
+ byRepository,
179
+ },
180
+ isLastPage: response.isLastPage,
181
+ ...(response.nextPageStart !== undefined && { nextPageStart: response.nextPageStart }),
182
+ };
183
+ }
package/src/index.ts CHANGED
@@ -163,4 +163,14 @@ server.tool(
163
163
  }
164
164
  );
165
165
 
166
+ server.tool(
167
+ "bitbucket_getInboxPullRequests",
168
+ "Get pull requests from the authenticated user's inbox that need their review. Returns PRs across all repositories where the user is a reviewer.",
169
+ bitbucketToolSchemas.getInboxPullRequests,
170
+ async ({ start, limit }) => {
171
+ const result = await bitbucketService.getInboxPullRequests(start, limit);
172
+ return formatToolResponse(result);
173
+ }
174
+ );
175
+
166
176
  await connectServer(server);