@zhanglc77/bitbucket-mcp-server 1.0.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +280 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1090 -0
  4. package/build/handlers/branch-handlers.d.ts +55 -0
  5. package/build/handlers/branch-handlers.d.ts.map +1 -0
  6. package/build/handlers/branch-handlers.js +522 -0
  7. package/build/handlers/branch-handlers.js.map +1 -0
  8. package/build/handlers/file-handlers.d.ts +33 -0
  9. package/build/handlers/file-handlers.d.ts.map +1 -0
  10. package/build/handlers/file-handlers.js +308 -0
  11. package/build/handlers/file-handlers.js.map +1 -0
  12. package/build/handlers/pull-request-handlers.d.ts +101 -0
  13. package/build/handlers/pull-request-handlers.d.ts.map +1 -0
  14. package/build/handlers/pull-request-handlers.js +955 -0
  15. package/build/handlers/pull-request-handlers.js.map +1 -0
  16. package/build/handlers/review-handlers.d.ts +67 -0
  17. package/build/handlers/review-handlers.d.ts.map +1 -0
  18. package/build/handlers/review-handlers.js +252 -0
  19. package/build/handlers/review-handlers.js.map +1 -0
  20. package/build/handlers/search-handlers.d.ts +20 -0
  21. package/build/handlers/search-handlers.d.ts.map +1 -0
  22. package/build/handlers/search-handlers.js +151 -0
  23. package/build/handlers/search-handlers.js.map +1 -0
  24. package/build/index.d.ts +3 -0
  25. package/build/index.d.ts.map +1 -0
  26. package/build/index.js +120 -0
  27. package/build/index.js.map +1 -0
  28. package/build/tools/definitions.d.ts +1191 -0
  29. package/build/tools/definitions.d.ts.map +1 -0
  30. package/build/tools/definitions.js +655 -0
  31. package/build/tools/definitions.js.map +1 -0
  32. package/build/types/bitbucket.d.ts +483 -0
  33. package/build/types/bitbucket.d.ts.map +1 -0
  34. package/build/types/bitbucket.js +2 -0
  35. package/build/types/bitbucket.js.map +1 -0
  36. package/build/types/guards.d.ts +140 -0
  37. package/build/types/guards.d.ts.map +1 -0
  38. package/build/types/guards.js +140 -0
  39. package/build/types/guards.js.map +1 -0
  40. package/build/utils/api-client.d.ts +22 -0
  41. package/build/utils/api-client.d.ts.map +1 -0
  42. package/build/utils/api-client.js +111 -0
  43. package/build/utils/api-client.js.map +1 -0
  44. package/build/utils/diff-parser.d.ts +42 -0
  45. package/build/utils/diff-parser.d.ts.map +1 -0
  46. package/build/utils/diff-parser.js +165 -0
  47. package/build/utils/diff-parser.js.map +1 -0
  48. package/build/utils/formatters.d.ts +8 -0
  49. package/build/utils/formatters.d.ts.map +1 -0
  50. package/build/utils/formatters.js +207 -0
  51. package/build/utils/formatters.js.map +1 -0
  52. package/build/utils/suggestion-formatter.d.ts +6 -0
  53. package/build/utils/suggestion-formatter.d.ts.map +1 -0
  54. package/build/utils/suggestion-formatter.js +17 -0
  55. package/build/utils/suggestion-formatter.js.map +1 -0
  56. package/package.json +55 -0
@@ -0,0 +1,955 @@
1
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ import { formatServerResponse, formatCloudResponse, formatServerCommit, formatCloudCommit } from '../utils/formatters.js';
3
+ import { formatSuggestionComment } from '../utils/suggestion-formatter.js';
4
+ import { DiffParser } from '../utils/diff-parser.js';
5
+ import { isGetPullRequestArgs, isListPullRequestsArgs, isCreatePullRequestArgs, isUpdatePullRequestArgs, isAddCommentArgs, isMergePullRequestArgs, isListPrCommitsArgs } from '../types/guards.js';
6
+ export class PullRequestHandlers {
7
+ apiClient;
8
+ baseUrl;
9
+ username;
10
+ constructor(apiClient, baseUrl, username) {
11
+ this.apiClient = apiClient;
12
+ this.baseUrl = baseUrl;
13
+ this.username = username;
14
+ }
15
+ async getFilteredPullRequestDiff(workspace, repository, pullRequestId, filePath, contextLines = 3) {
16
+ let apiPath;
17
+ let config = {};
18
+ if (this.apiClient.getIsServer()) {
19
+ // Bitbucket Server API
20
+ apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/diff`;
21
+ config.params = { contextLines };
22
+ }
23
+ else {
24
+ // Bitbucket Cloud API
25
+ apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diff`;
26
+ config.params = { context: contextLines };
27
+ }
28
+ config.headers = { 'Accept': 'text/plain' };
29
+ const rawDiff = await this.apiClient.makeRequest('get', apiPath, undefined, config);
30
+ const diffParser = new DiffParser();
31
+ const sections = diffParser.parseDiffIntoSections(rawDiff);
32
+ const filterOptions = {
33
+ filePath: filePath
34
+ };
35
+ const filteredResult = diffParser.filterSections(sections, filterOptions);
36
+ const filteredDiff = diffParser.reconstructDiff(filteredResult.sections);
37
+ return filteredDiff;
38
+ }
39
+ async handleGetPullRequest(args) {
40
+ if (!isGetPullRequestArgs(args)) {
41
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for get_pull_request');
42
+ }
43
+ const { workspace, repository, pull_request_id } = args;
44
+ try {
45
+ const apiPath = this.apiClient.getIsServer()
46
+ ? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
47
+ : `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
48
+ const pr = await this.apiClient.makeRequest('get', apiPath);
49
+ let mergeInfo = {};
50
+ if (this.apiClient.getIsServer() && pr.state === 'MERGED') {
51
+ try {
52
+ const activitiesPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/activities`;
53
+ const activitiesResponse = await this.apiClient.makeRequest('get', activitiesPath, undefined, {
54
+ params: { limit: 100 }
55
+ });
56
+ const activities = activitiesResponse.values || [];
57
+ const mergeActivity = activities.find((a) => a.action === 'MERGED');
58
+ if (mergeActivity) {
59
+ mergeInfo.mergeCommitHash = mergeActivity.commit?.id || null;
60
+ mergeInfo.mergedBy = mergeActivity.user?.displayName || null;
61
+ mergeInfo.mergedAt = new Date(mergeActivity.createdDate).toISOString();
62
+ if (mergeActivity.commit?.id) {
63
+ try {
64
+ const commitPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits/${mergeActivity.commit.id}`;
65
+ const commitResponse = await this.apiClient.makeRequest('get', commitPath);
66
+ mergeInfo.mergeCommitMessage = commitResponse.message || null;
67
+ }
68
+ catch (commitError) {
69
+ console.error('Failed to fetch merge commit message:', commitError);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ catch (activitiesError) {
75
+ console.error('Failed to fetch PR activities:', activitiesError);
76
+ }
77
+ }
78
+ let comments = [];
79
+ let activeCommentCount = 0;
80
+ let totalCommentCount = 0;
81
+ let fileChanges = [];
82
+ let fileChangesSummary = null;
83
+ try {
84
+ const [commentsResult, fileChangesResult] = await Promise.all([
85
+ this.fetchPullRequestComments(workspace, repository, pull_request_id),
86
+ this.fetchPullRequestFileChanges(workspace, repository, pull_request_id)
87
+ ]);
88
+ comments = commentsResult.comments;
89
+ activeCommentCount = commentsResult.activeCount;
90
+ totalCommentCount = commentsResult.totalCount;
91
+ fileChanges = fileChangesResult.fileChanges;
92
+ fileChangesSummary = fileChangesResult.summary;
93
+ }
94
+ catch (error) {
95
+ console.error('Failed to fetch additional PR data:', error);
96
+ }
97
+ const formattedResponse = this.apiClient.getIsServer()
98
+ ? formatServerResponse(pr, mergeInfo, this.baseUrl)
99
+ : formatCloudResponse(pr);
100
+ const enhancedResponse = {
101
+ ...formattedResponse,
102
+ active_comments: comments,
103
+ active_comment_count: activeCommentCount,
104
+ total_comment_count: totalCommentCount,
105
+ file_changes: fileChanges,
106
+ file_changes_summary: fileChangesSummary
107
+ };
108
+ return {
109
+ content: [
110
+ {
111
+ type: 'text',
112
+ text: JSON.stringify(enhancedResponse, null, 2),
113
+ },
114
+ ],
115
+ };
116
+ }
117
+ catch (error) {
118
+ return this.apiClient.handleApiError(error, `getting pull request ${pull_request_id} in ${workspace}/${repository}`);
119
+ }
120
+ }
121
+ async handleListPullRequests(args) {
122
+ if (!isListPullRequestsArgs(args)) {
123
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for list_pull_requests');
124
+ }
125
+ const { workspace, repository, state = 'OPEN', author, limit = 25, start = 0 } = args;
126
+ try {
127
+ let apiPath;
128
+ let params = {};
129
+ if (this.apiClient.getIsServer()) {
130
+ // Bitbucket Server API
131
+ apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
132
+ params = {
133
+ state: state === 'ALL' ? undefined : state,
134
+ limit,
135
+ start,
136
+ };
137
+ if (author) {
138
+ params['role.1'] = 'AUTHOR';
139
+ params['username.1'] = author;
140
+ }
141
+ }
142
+ else {
143
+ // Bitbucket Cloud API
144
+ apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
145
+ params = {
146
+ state: state === 'ALL' ? undefined : state,
147
+ pagelen: limit,
148
+ page: Math.floor(start / limit) + 1,
149
+ };
150
+ if (author) {
151
+ params['q'] = `author.username="${author}"`;
152
+ }
153
+ }
154
+ const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params });
155
+ let pullRequests = [];
156
+ let totalCount = 0;
157
+ let nextPageStart = null;
158
+ if (this.apiClient.getIsServer()) {
159
+ pullRequests = (response.values || []).map((pr) => formatServerResponse(pr, undefined, this.baseUrl));
160
+ totalCount = response.size || 0;
161
+ if (!response.isLastPage && response.nextPageStart !== undefined) {
162
+ nextPageStart = response.nextPageStart;
163
+ }
164
+ }
165
+ else {
166
+ pullRequests = (response.values || []).map((pr) => formatCloudResponse(pr));
167
+ totalCount = response.size || 0;
168
+ if (response.next) {
169
+ nextPageStart = start + limit;
170
+ }
171
+ }
172
+ return {
173
+ content: [
174
+ {
175
+ type: 'text',
176
+ text: JSON.stringify({
177
+ pull_requests: pullRequests,
178
+ total_count: totalCount,
179
+ start,
180
+ limit,
181
+ has_more: nextPageStart !== null,
182
+ next_start: nextPageStart,
183
+ }, null, 2),
184
+ },
185
+ ],
186
+ };
187
+ }
188
+ catch (error) {
189
+ return this.apiClient.handleApiError(error, `listing pull requests in ${workspace}/${repository}`);
190
+ }
191
+ }
192
+ async handleCreatePullRequest(args) {
193
+ if (!isCreatePullRequestArgs(args)) {
194
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for create_pull_request');
195
+ }
196
+ const { workspace, repository, title, source_branch, destination_branch, description, reviewers, close_source_branch } = args;
197
+ try {
198
+ let apiPath;
199
+ let requestBody;
200
+ if (this.apiClient.getIsServer()) {
201
+ // Bitbucket Server API
202
+ apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
203
+ requestBody = {
204
+ title,
205
+ description: description || '',
206
+ fromRef: {
207
+ id: `refs/heads/${source_branch}`,
208
+ repository: {
209
+ slug: repository,
210
+ project: {
211
+ key: workspace
212
+ }
213
+ }
214
+ },
215
+ toRef: {
216
+ id: `refs/heads/${destination_branch}`,
217
+ repository: {
218
+ slug: repository,
219
+ project: {
220
+ key: workspace
221
+ }
222
+ }
223
+ },
224
+ reviewers: reviewers?.map(r => ({ user: { name: r } })) || []
225
+ };
226
+ }
227
+ else {
228
+ // Bitbucket Cloud API
229
+ apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
230
+ requestBody = {
231
+ title,
232
+ description: description || '',
233
+ source: {
234
+ branch: {
235
+ name: source_branch
236
+ }
237
+ },
238
+ destination: {
239
+ branch: {
240
+ name: destination_branch
241
+ }
242
+ },
243
+ close_source_branch: close_source_branch || false,
244
+ reviewers: reviewers?.map(r => ({ username: r })) || []
245
+ };
246
+ }
247
+ const pr = await this.apiClient.makeRequest('post', apiPath, requestBody);
248
+ const formattedResponse = this.apiClient.getIsServer()
249
+ ? formatServerResponse(pr, undefined, this.baseUrl)
250
+ : formatCloudResponse(pr);
251
+ return {
252
+ content: [
253
+ {
254
+ type: 'text',
255
+ text: JSON.stringify({
256
+ message: 'Pull request created successfully',
257
+ pull_request: formattedResponse
258
+ }, null, 2),
259
+ },
260
+ ],
261
+ };
262
+ }
263
+ catch (error) {
264
+ return this.apiClient.handleApiError(error, `creating pull request in ${workspace}/${repository}`);
265
+ }
266
+ }
267
+ async handleUpdatePullRequest(args) {
268
+ if (!isUpdatePullRequestArgs(args)) {
269
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for update_pull_request');
270
+ }
271
+ const { workspace, repository, pull_request_id, title, description, destination_branch, reviewers } = args;
272
+ try {
273
+ let apiPath;
274
+ let requestBody = {};
275
+ if (this.apiClient.getIsServer()) {
276
+ // Bitbucket Server API
277
+ apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
278
+ // First get the current PR to get version number and existing data
279
+ const currentPr = await this.apiClient.makeRequest('get', apiPath);
280
+ requestBody.version = currentPr.version;
281
+ if (title !== undefined)
282
+ requestBody.title = title;
283
+ if (description !== undefined)
284
+ requestBody.description = description;
285
+ if (destination_branch !== undefined) {
286
+ requestBody.toRef = {
287
+ id: `refs/heads/${destination_branch}`,
288
+ repository: {
289
+ slug: repository,
290
+ project: {
291
+ key: workspace
292
+ }
293
+ }
294
+ };
295
+ }
296
+ // Handle reviewers: preserve existing ones if not explicitly updating
297
+ if (reviewers !== undefined) {
298
+ // User wants to update reviewers
299
+ // Create a map of existing reviewers for preservation of approval status
300
+ const existingReviewersMap = new Map(currentPr.reviewers.map((r) => [r.user.name, r]));
301
+ requestBody.reviewers = reviewers.map(username => {
302
+ const existing = existingReviewersMap.get(username);
303
+ if (existing) {
304
+ // Preserve existing reviewer's full data including approval status
305
+ return existing;
306
+ }
307
+ else {
308
+ // Add new reviewer (without approval status)
309
+ return { user: { name: username } };
310
+ }
311
+ });
312
+ }
313
+ else {
314
+ // No reviewers provided - preserve existing reviewers with their full data
315
+ requestBody.reviewers = currentPr.reviewers;
316
+ }
317
+ }
318
+ else {
319
+ // Bitbucket Cloud API
320
+ apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
321
+ if (title !== undefined)
322
+ requestBody.title = title;
323
+ if (description !== undefined)
324
+ requestBody.description = description;
325
+ if (destination_branch !== undefined) {
326
+ requestBody.destination = {
327
+ branch: {
328
+ name: destination_branch
329
+ }
330
+ };
331
+ }
332
+ if (reviewers !== undefined) {
333
+ requestBody.reviewers = reviewers.map(r => ({ username: r }));
334
+ }
335
+ }
336
+ const pr = await this.apiClient.makeRequest('put', apiPath, requestBody);
337
+ const formattedResponse = this.apiClient.getIsServer()
338
+ ? formatServerResponse(pr, undefined, this.baseUrl)
339
+ : formatCloudResponse(pr);
340
+ return {
341
+ content: [
342
+ {
343
+ type: 'text',
344
+ text: JSON.stringify({
345
+ message: 'Pull request updated successfully',
346
+ pull_request: formattedResponse
347
+ }, null, 2),
348
+ },
349
+ ],
350
+ };
351
+ }
352
+ catch (error) {
353
+ return this.apiClient.handleApiError(error, `updating pull request ${pull_request_id} in ${workspace}/${repository}`);
354
+ }
355
+ }
356
+ async handleAddComment(args) {
357
+ if (!isAddCommentArgs(args)) {
358
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for add_comment');
359
+ }
360
+ let { workspace, repository, pull_request_id, comment_text, parent_comment_id, file_path, line_number, line_type, suggestion, suggestion_end_line, code_snippet, search_context, match_strategy = 'strict' } = args;
361
+ let sequentialPosition;
362
+ if (code_snippet && !line_number && file_path) {
363
+ try {
364
+ const resolved = await this.resolveLineFromCode(workspace, repository, pull_request_id, file_path, code_snippet, search_context, match_strategy);
365
+ line_number = resolved.line_number;
366
+ line_type = resolved.line_type;
367
+ sequentialPosition = resolved.sequential_position;
368
+ }
369
+ catch (error) {
370
+ throw error;
371
+ }
372
+ }
373
+ if (suggestion && (!file_path || !line_number)) {
374
+ throw new McpError(ErrorCode.InvalidParams, 'Suggestions require file_path and line_number to be specified');
375
+ }
376
+ const isInlineComment = file_path !== undefined && line_number !== undefined;
377
+ let finalCommentText = comment_text;
378
+ if (suggestion) {
379
+ finalCommentText = formatSuggestionComment(comment_text, suggestion, line_number, suggestion_end_line || line_number);
380
+ }
381
+ try {
382
+ let apiPath;
383
+ let requestBody;
384
+ if (this.apiClient.getIsServer()) {
385
+ // Bitbucket Server API
386
+ apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/comments`;
387
+ requestBody = {
388
+ text: finalCommentText
389
+ };
390
+ if (parent_comment_id !== undefined) {
391
+ requestBody.parent = { id: parent_comment_id };
392
+ }
393
+ if (isInlineComment) {
394
+ requestBody.anchor = {
395
+ line: line_number,
396
+ lineType: line_type || 'CONTEXT',
397
+ fileType: line_type === 'REMOVED' ? 'FROM' : 'TO',
398
+ path: file_path,
399
+ diffType: 'EFFECTIVE'
400
+ };
401
+ }
402
+ }
403
+ else {
404
+ // Bitbucket Cloud API
405
+ apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/comments`;
406
+ requestBody = {
407
+ content: {
408
+ raw: finalCommentText
409
+ }
410
+ };
411
+ if (parent_comment_id !== undefined) {
412
+ requestBody.parent = { id: parent_comment_id };
413
+ }
414
+ if (isInlineComment) {
415
+ requestBody.inline = {
416
+ to: line_number,
417
+ path: file_path
418
+ };
419
+ }
420
+ }
421
+ const comment = await this.apiClient.makeRequest('post', apiPath, requestBody);
422
+ const responseMessage = suggestion
423
+ ? 'Comment with code suggestion added successfully'
424
+ : (isInlineComment ? 'Inline comment added successfully' : 'Comment added successfully');
425
+ return {
426
+ content: [
427
+ {
428
+ type: 'text',
429
+ text: JSON.stringify({
430
+ message: responseMessage,
431
+ comment: {
432
+ id: comment.id,
433
+ text: this.apiClient.getIsServer() ? comment.text : comment.content.raw,
434
+ author: this.apiClient.getIsServer() ? comment.author.displayName : comment.user.display_name,
435
+ created_on: this.apiClient.getIsServer() ? new Date(comment.createdDate).toLocaleString() : comment.created_on,
436
+ file_path: isInlineComment ? file_path : undefined,
437
+ line_number: isInlineComment ? line_number : undefined,
438
+ line_type: isInlineComment ? (line_type || 'CONTEXT') : undefined,
439
+ has_suggestion: !!suggestion,
440
+ suggestion_lines: suggestion ? (suggestion_end_line ? `${line_number}-${suggestion_end_line}` : `${line_number}`) : undefined
441
+ }
442
+ }, null, 2),
443
+ },
444
+ ],
445
+ };
446
+ }
447
+ catch (error) {
448
+ return this.apiClient.handleApiError(error, `adding ${isInlineComment ? 'inline ' : ''}comment to pull request ${pull_request_id} in ${workspace}/${repository}`);
449
+ }
450
+ }
451
+ async handleMergePullRequest(args) {
452
+ if (!isMergePullRequestArgs(args)) {
453
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for merge_pull_request');
454
+ }
455
+ const { workspace, repository, pull_request_id, merge_strategy, close_source_branch, commit_message } = args;
456
+ try {
457
+ let apiPath;
458
+ let requestBody = {};
459
+ if (this.apiClient.getIsServer()) {
460
+ // Bitbucket Server API
461
+ apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/merge`;
462
+ // Get current PR version
463
+ const prPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
464
+ const currentPr = await this.apiClient.makeRequest('get', prPath);
465
+ requestBody.version = currentPr.version;
466
+ if (commit_message) {
467
+ requestBody.message = commit_message;
468
+ }
469
+ }
470
+ else {
471
+ // Bitbucket Cloud API
472
+ apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/merge`;
473
+ if (merge_strategy) {
474
+ requestBody.merge_strategy = merge_strategy;
475
+ }
476
+ if (close_source_branch !== undefined) {
477
+ requestBody.close_source_branch = close_source_branch;
478
+ }
479
+ if (commit_message) {
480
+ requestBody.message = commit_message;
481
+ }
482
+ }
483
+ const result = await this.apiClient.makeRequest('post', apiPath, requestBody);
484
+ return {
485
+ content: [
486
+ {
487
+ type: 'text',
488
+ text: JSON.stringify({
489
+ message: 'Pull request merged successfully',
490
+ merge_commit: this.apiClient.getIsServer() ? result.properties?.mergeCommit : result.merge_commit?.hash,
491
+ pull_request_id
492
+ }, null, 2),
493
+ },
494
+ ],
495
+ };
496
+ }
497
+ catch (error) {
498
+ return this.apiClient.handleApiError(error, `merging pull request ${pull_request_id} in ${workspace}/${repository}`);
499
+ }
500
+ }
501
+ async fetchPullRequestComments(workspace, repository, pullRequestId) {
502
+ try {
503
+ let comments = [];
504
+ let activeCount = 0;
505
+ let totalCount = 0;
506
+ if (this.apiClient.getIsServer()) {
507
+ const processNestedComments = (comment, anchor) => {
508
+ const formattedComment = {
509
+ id: comment.id,
510
+ author: comment.author.displayName,
511
+ text: comment.text,
512
+ created_on: new Date(comment.createdDate).toISOString(),
513
+ is_inline: !!anchor,
514
+ file_path: anchor?.path,
515
+ line_number: anchor?.line,
516
+ state: comment.state
517
+ };
518
+ if (comment.comments && comment.comments.length > 0) {
519
+ formattedComment.replies = comment.comments
520
+ .filter((reply) => {
521
+ if (reply.state === 'RESOLVED')
522
+ return false;
523
+ if (anchor && anchor.orphaned === true)
524
+ return false;
525
+ return true;
526
+ })
527
+ .map((reply) => processNestedComments(reply, anchor));
528
+ }
529
+ return formattedComment;
530
+ };
531
+ const countAllComments = (comment) => {
532
+ let count = 1;
533
+ if (comment.comments && comment.comments.length > 0) {
534
+ count += comment.comments.reduce((sum, reply) => sum + countAllComments(reply), 0);
535
+ }
536
+ return count;
537
+ };
538
+ const countActiveComments = (comment, anchor) => {
539
+ let count = 0;
540
+ if (comment.state !== 'RESOLVED' && (!anchor || anchor.orphaned !== true)) {
541
+ count = 1;
542
+ }
543
+ if (comment.comments && comment.comments.length > 0) {
544
+ count += comment.comments.reduce((sum, reply) => sum + countActiveComments(reply, anchor), 0);
545
+ }
546
+ return count;
547
+ };
548
+ const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/activities`;
549
+ const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
550
+ params: { limit: 1000 }
551
+ });
552
+ const activities = response.values || [];
553
+ const commentActivities = activities.filter((a) => a.action === 'COMMENTED' && a.comment);
554
+ totalCount = commentActivities.reduce((sum, activity) => {
555
+ return sum + countAllComments(activity.comment);
556
+ }, 0);
557
+ activeCount = commentActivities.reduce((sum, activity) => {
558
+ return sum + countActiveComments(activity.comment, activity.commentAnchor);
559
+ }, 0);
560
+ const processedComments = commentActivities
561
+ .filter((a) => {
562
+ const c = a.comment;
563
+ const anchor = a.commentAnchor;
564
+ if (c.state === 'RESOLVED')
565
+ return false;
566
+ if (anchor && anchor.orphaned === true)
567
+ return false;
568
+ return true;
569
+ })
570
+ .map((a) => processNestedComments(a.comment, a.commentAnchor));
571
+ comments = processedComments.slice(0, 20);
572
+ }
573
+ else {
574
+ const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/comments`;
575
+ const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
576
+ params: { pagelen: 100 }
577
+ });
578
+ const allComments = response.values || [];
579
+ totalCount = allComments.length;
580
+ const activeComments = allComments
581
+ .filter((c) => !c.deleted && !c.resolved)
582
+ .slice(0, 20);
583
+ activeCount = allComments.filter((c) => !c.deleted && !c.resolved).length;
584
+ comments = activeComments.map((c) => ({
585
+ id: c.id,
586
+ author: c.user.display_name,
587
+ text: c.content.raw,
588
+ created_on: c.created_on,
589
+ is_inline: !!c.inline,
590
+ file_path: c.inline?.path,
591
+ line_number: c.inline?.to
592
+ }));
593
+ }
594
+ return { comments, activeCount, totalCount };
595
+ }
596
+ catch (error) {
597
+ console.error('Failed to fetch comments:', error);
598
+ return { comments: [], activeCount: 0, totalCount: 0 };
599
+ }
600
+ }
601
+ async fetchPullRequestFileChanges(workspace, repository, pullRequestId) {
602
+ try {
603
+ let fileChanges = [];
604
+ let totalLinesAdded = 0;
605
+ let totalLinesRemoved = 0;
606
+ if (this.apiClient.getIsServer()) {
607
+ const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/changes`;
608
+ const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
609
+ params: { limit: 1000 }
610
+ });
611
+ const changes = response.values || [];
612
+ fileChanges = changes.map((change) => {
613
+ let status = 'modified';
614
+ if (change.type === 'ADD')
615
+ status = 'added';
616
+ else if (change.type === 'DELETE')
617
+ status = 'removed';
618
+ else if (change.type === 'MOVE' || change.type === 'RENAME')
619
+ status = 'renamed';
620
+ return {
621
+ path: change.path.toString,
622
+ status,
623
+ old_path: change.srcPath?.toString
624
+ };
625
+ });
626
+ }
627
+ else {
628
+ const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diffstat`;
629
+ const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
630
+ params: { pagelen: 100 }
631
+ });
632
+ const diffstats = response.values || [];
633
+ fileChanges = diffstats.map((stat) => {
634
+ totalLinesAdded += stat.lines_added;
635
+ totalLinesRemoved += stat.lines_removed;
636
+ return {
637
+ path: stat.path,
638
+ status: stat.type,
639
+ old_path: stat.old?.path
640
+ };
641
+ });
642
+ }
643
+ const summary = {
644
+ total_files: fileChanges.length
645
+ };
646
+ return { fileChanges, summary };
647
+ }
648
+ catch (error) {
649
+ console.error('Failed to fetch file changes:', error);
650
+ return {
651
+ fileChanges: [],
652
+ summary: {
653
+ total_files: 0
654
+ }
655
+ };
656
+ }
657
+ }
658
+ async resolveLineFromCode(workspace, repository, pullRequestId, filePath, codeSnippet, searchContext, matchStrategy = 'strict') {
659
+ try {
660
+ const diffContent = await this.getFilteredPullRequestDiff(workspace, repository, pullRequestId, filePath);
661
+ const parser = new DiffParser();
662
+ const sections = parser.parseDiffIntoSections(diffContent);
663
+ let fileSection = sections[0];
664
+ if (!this.apiClient.getIsServer()) {
665
+ fileSection = sections.find(s => s.filePath === filePath) || sections[0];
666
+ }
667
+ if (!fileSection) {
668
+ throw new McpError(ErrorCode.InvalidParams, `File ${filePath} not found in pull request diff`);
669
+ }
670
+ const matches = this.findCodeMatches(fileSection.content, codeSnippet, searchContext);
671
+ if (matches.length === 0) {
672
+ throw new McpError(ErrorCode.InvalidParams, `Code snippet not found in ${filePath}`);
673
+ }
674
+ if (matches.length === 1) {
675
+ return {
676
+ line_number: matches[0].line_number,
677
+ line_type: matches[0].line_type,
678
+ sequential_position: matches[0].sequential_position,
679
+ hunk_info: matches[0].hunk_info,
680
+ diff_context: matches[0].preview,
681
+ diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
682
+ calculation_details: `Direct line number from diff: ${matches[0].line_number}`
683
+ };
684
+ }
685
+ if (matchStrategy === 'best') {
686
+ const best = this.selectBestMatch(matches);
687
+ return {
688
+ line_number: best.line_number,
689
+ line_type: best.line_type,
690
+ sequential_position: best.sequential_position,
691
+ hunk_info: best.hunk_info,
692
+ diff_context: best.preview,
693
+ diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
694
+ calculation_details: `Best match selected from ${matches.length} matches, line: ${best.line_number}`
695
+ };
696
+ }
697
+ const error = {
698
+ code: 'MULTIPLE_MATCHES_FOUND',
699
+ message: `Code snippet '${codeSnippet.substring(0, 50)}...' found in ${matches.length} locations`,
700
+ occurrences: matches.map(m => ({
701
+ line_number: m.line_number,
702
+ file_path: filePath,
703
+ preview: m.preview,
704
+ confidence: m.confidence,
705
+ line_type: m.line_type
706
+ })),
707
+ suggestion: 'To resolve, either:\n1. Add more context to uniquely identify the location\n2. Use match_strategy: \'best\' to auto-select highest confidence match\n3. Use line_number directly'
708
+ };
709
+ throw new McpError(ErrorCode.InvalidParams, JSON.stringify({ error }));
710
+ }
711
+ catch (error) {
712
+ if (error instanceof McpError) {
713
+ throw error;
714
+ }
715
+ throw new McpError(ErrorCode.InternalError, `Failed to resolve line from code: ${error instanceof Error ? error.message : String(error)}`);
716
+ }
717
+ }
718
+ findCodeMatches(diffContent, codeSnippet, searchContext) {
719
+ const lines = diffContent.split('\n');
720
+ const matches = [];
721
+ let currentDestLine = 0; // Destination file line number
722
+ let currentSrcLine = 0; // Source file line number
723
+ let inHunk = false;
724
+ let sequentialAddedCount = 0; // Track sequential ADDED lines
725
+ let currentHunkIndex = -1;
726
+ let currentHunkDestStart = 0;
727
+ let currentHunkSrcStart = 0;
728
+ let destPositionInHunk = 0; // Track position in destination file relative to hunk start
729
+ let srcPositionInHunk = 0; // Track position in source file relative to hunk start
730
+ for (let i = 0; i < lines.length; i++) {
731
+ const line = lines[i];
732
+ if (line.startsWith('@@')) {
733
+ const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
734
+ if (match) {
735
+ currentHunkSrcStart = parseInt(match[1]);
736
+ currentHunkDestStart = parseInt(match[2]);
737
+ currentSrcLine = currentHunkSrcStart;
738
+ currentDestLine = currentHunkDestStart;
739
+ inHunk = true;
740
+ currentHunkIndex++;
741
+ destPositionInHunk = 0;
742
+ srcPositionInHunk = 0;
743
+ continue;
744
+ }
745
+ }
746
+ if (!inHunk)
747
+ continue;
748
+ if (line === '') {
749
+ inHunk = false;
750
+ continue;
751
+ }
752
+ let lineType;
753
+ let lineContent = '';
754
+ let lineNumber = 0;
755
+ if (line.startsWith('+')) {
756
+ lineType = 'ADDED';
757
+ lineContent = line.substring(1);
758
+ lineNumber = currentHunkDestStart + destPositionInHunk;
759
+ destPositionInHunk++;
760
+ sequentialAddedCount++;
761
+ }
762
+ else if (line.startsWith('-')) {
763
+ lineType = 'REMOVED';
764
+ lineContent = line.substring(1);
765
+ lineNumber = currentHunkSrcStart + srcPositionInHunk;
766
+ srcPositionInHunk++;
767
+ }
768
+ else if (line.startsWith(' ')) {
769
+ lineType = 'CONTEXT';
770
+ lineContent = line.substring(1);
771
+ lineNumber = currentHunkDestStart + destPositionInHunk;
772
+ destPositionInHunk++;
773
+ srcPositionInHunk++;
774
+ }
775
+ else {
776
+ inHunk = false;
777
+ continue;
778
+ }
779
+ if (lineContent.trim() === codeSnippet.trim()) {
780
+ const confidence = this.calculateConfidence(lines, i, searchContext, lineType);
781
+ matches.push({
782
+ line_number: lineNumber,
783
+ line_type: lineType,
784
+ exact_content: codeSnippet,
785
+ preview: this.getPreview(lines, i),
786
+ confidence,
787
+ context: this.extractContext(lines, i),
788
+ sequential_position: lineType === 'ADDED' ? sequentialAddedCount : undefined,
789
+ hunk_info: {
790
+ hunk_index: currentHunkIndex,
791
+ destination_start: currentHunkDestStart,
792
+ line_in_hunk: destPositionInHunk
793
+ }
794
+ });
795
+ }
796
+ if (lineType === 'ADDED') {
797
+ currentDestLine++;
798
+ }
799
+ else if (lineType === 'REMOVED') {
800
+ currentSrcLine++;
801
+ }
802
+ else if (lineType === 'CONTEXT') {
803
+ currentSrcLine++;
804
+ currentDestLine++;
805
+ }
806
+ }
807
+ return matches;
808
+ }
809
+ calculateConfidence(lines, index, searchContext, lineType) {
810
+ let confidence = 0.5; // Base confidence
811
+ if (!searchContext) {
812
+ return confidence;
813
+ }
814
+ if (searchContext.before) {
815
+ let matchedBefore = 0;
816
+ for (let j = 0; j < searchContext.before.length; j++) {
817
+ const contextLine = searchContext.before[searchContext.before.length - 1 - j];
818
+ const checkIndex = index - j - 1;
819
+ if (checkIndex >= 0) {
820
+ const checkLine = lines[checkIndex].substring(1);
821
+ if (checkLine.trim() === contextLine.trim()) {
822
+ matchedBefore++;
823
+ }
824
+ }
825
+ }
826
+ confidence += (matchedBefore / searchContext.before.length) * 0.3;
827
+ }
828
+ if (searchContext.after) {
829
+ let matchedAfter = 0;
830
+ for (let j = 0; j < searchContext.after.length; j++) {
831
+ const contextLine = searchContext.after[j];
832
+ const checkIndex = index + j + 1;
833
+ if (checkIndex < lines.length) {
834
+ const checkLine = lines[checkIndex].substring(1);
835
+ if (checkLine.trim() === contextLine.trim()) {
836
+ matchedAfter++;
837
+ }
838
+ }
839
+ }
840
+ confidence += (matchedAfter / searchContext.after.length) * 0.3;
841
+ }
842
+ if (lineType === 'ADDED') {
843
+ confidence += 0.1;
844
+ }
845
+ return Math.min(confidence, 1.0);
846
+ }
847
+ getPreview(lines, index) {
848
+ const start = Math.max(0, index - 1);
849
+ const end = Math.min(lines.length, index + 2);
850
+ const previewLines = [];
851
+ for (let i = start; i < end; i++) {
852
+ const prefix = i === index ? '> ' : ' ';
853
+ previewLines.push(prefix + lines[i]);
854
+ }
855
+ return previewLines.join('\n');
856
+ }
857
+ extractContext(lines, index) {
858
+ const linesBefore = [];
859
+ const linesAfter = [];
860
+ for (let i = Math.max(0, index - 2); i < index; i++) {
861
+ if (lines[i].match(/^[+\- ]/)) {
862
+ linesBefore.push(lines[i].substring(1));
863
+ }
864
+ }
865
+ for (let i = index + 1; i < Math.min(lines.length, index + 3); i++) {
866
+ if (lines[i].match(/^[+\- ]/)) {
867
+ linesAfter.push(lines[i].substring(1));
868
+ }
869
+ }
870
+ return {
871
+ lines_before: linesBefore,
872
+ lines_after: linesAfter
873
+ };
874
+ }
875
+ selectBestMatch(matches) {
876
+ return matches.sort((a, b) => b.confidence - a.confidence)[0];
877
+ }
878
+ async handleListPrCommits(args) {
879
+ if (!isListPrCommitsArgs(args)) {
880
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for list_pr_commits');
881
+ }
882
+ const { workspace, repository, pull_request_id, limit = 25, start = 0 } = args;
883
+ try {
884
+ // First get the PR details to include in response
885
+ const prPath = this.apiClient.getIsServer()
886
+ ? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
887
+ : `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
888
+ let prTitle = '';
889
+ try {
890
+ const pr = await this.apiClient.makeRequest('get', prPath);
891
+ prTitle = pr.title;
892
+ }
893
+ catch (e) {
894
+ // Ignore error, PR title is optional
895
+ }
896
+ let apiPath;
897
+ let params = {};
898
+ let commits = [];
899
+ let totalCount = 0;
900
+ let nextPageStart = null;
901
+ if (this.apiClient.getIsServer()) {
902
+ // Bitbucket Server API
903
+ apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/commits`;
904
+ params = {
905
+ limit,
906
+ start,
907
+ withCounts: true
908
+ };
909
+ const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params });
910
+ // Format commits
911
+ commits = (response.values || []).map((commit) => formatServerCommit(commit));
912
+ totalCount = response.size || commits.length;
913
+ if (!response.isLastPage && response.nextPageStart !== undefined) {
914
+ nextPageStart = response.nextPageStart;
915
+ }
916
+ }
917
+ else {
918
+ // Bitbucket Cloud API
919
+ apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/commits`;
920
+ params = {
921
+ pagelen: limit,
922
+ page: Math.floor(start / limit) + 1
923
+ };
924
+ const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params });
925
+ // Format commits
926
+ commits = (response.values || []).map((commit) => formatCloudCommit(commit));
927
+ totalCount = response.size || commits.length;
928
+ if (response.next) {
929
+ nextPageStart = start + limit;
930
+ }
931
+ }
932
+ return {
933
+ content: [
934
+ {
935
+ type: 'text',
936
+ text: JSON.stringify({
937
+ pull_request_id,
938
+ pull_request_title: prTitle,
939
+ commits,
940
+ total_count: totalCount,
941
+ start,
942
+ limit,
943
+ has_more: nextPageStart !== null,
944
+ next_start: nextPageStart
945
+ }, null, 2),
946
+ },
947
+ ],
948
+ };
949
+ }
950
+ catch (error) {
951
+ return this.apiClient.handleApiError(error, `listing commits for pull request ${pull_request_id} in ${workspace}/${repository}`);
952
+ }
953
+ }
954
+ }
955
+ //# sourceMappingURL=pull-request-handlers.js.map