@zhanglc77/bitbucket-mcp-server
Version:
MCP server for Bitbucket API integration - supports both Cloud and Server
974 lines • 45 kB
JavaScript
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { formatServerResponse, formatCloudResponse, formatServerCommit, formatCloudCommit } from '../utils/formatters.js';
import { formatSuggestionComment } from '../utils/suggestion-formatter.js';
import { DiffParser } from '../utils/diff-parser.js';
import { applyFieldsFilter } from '../utils/field-filter.js';
import { isGetPullRequestArgs, isListPullRequestsArgs, isCreatePullRequestArgs, isUpdatePullRequestArgs, isAddCommentArgs, isMergePullRequestArgs, isListPrCommitsArgs } from '../types/guards.js';
export class PullRequestHandlers {
apiClient;
baseUrl;
username;
constructor(apiClient, baseUrl, username) {
this.apiClient = apiClient;
this.baseUrl = baseUrl;
this.username = username;
}
async getFilteredPullRequestDiff(workspace, repository, pullRequestId, filePath, contextLines = 3) {
let apiPath;
let config = {};
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/diff`;
config.params = { contextLines };
}
else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diff`;
config.params = { context: contextLines };
}
config.headers = { 'Accept': 'text/plain' };
const rawDiff = await this.apiClient.makeRequest('get', apiPath, undefined, config);
const diffParser = new DiffParser();
const sections = diffParser.parseDiffIntoSections(rawDiff);
const filterOptions = {
filePath: filePath
};
const filteredResult = diffParser.filterSections(sections, filterOptions);
const filteredDiff = diffParser.reconstructDiff(filteredResult.sections);
return filteredDiff;
}
async handleGetPullRequest(args) {
if (!isGetPullRequestArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for get_pull_request');
}
const { workspace, repository, pull_request_id } = args;
try {
const apiPath = this.apiClient.getIsServer()
? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
: `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
const pr = await this.apiClient.makeRequest('get', apiPath);
let mergeInfo = {};
if (this.apiClient.getIsServer() && pr.state === 'MERGED') {
try {
const activitiesPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/activities`;
const activitiesResponse = await this.apiClient.makeRequest('get', activitiesPath, undefined, {
params: { limit: 100 }
});
const activities = activitiesResponse.values || [];
const mergeActivity = activities.find((a) => a.action === 'MERGED');
if (mergeActivity) {
mergeInfo.mergeCommitHash = mergeActivity.commit?.id || null;
mergeInfo.mergedBy = mergeActivity.user?.displayName || null;
mergeInfo.mergedAt = new Date(mergeActivity.createdDate).toISOString();
if (mergeActivity.commit?.id) {
try {
const commitPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits/${mergeActivity.commit.id}`;
const commitResponse = await this.apiClient.makeRequest('get', commitPath);
mergeInfo.mergeCommitMessage = commitResponse.message || null;
}
catch (commitError) {
console.error('Failed to fetch merge commit message:', commitError);
}
}
}
}
catch (activitiesError) {
console.error('Failed to fetch PR activities:', activitiesError);
}
}
let comments = [];
let activeCommentCount = 0;
let totalCommentCount = 0;
let fileChanges = [];
let fileChangesSummary = null;
try {
const [commentsResult, fileChangesResult] = await Promise.all([
this.fetchPullRequestComments(workspace, repository, pull_request_id),
this.fetchPullRequestFileChanges(workspace, repository, pull_request_id)
]);
comments = commentsResult.comments;
activeCommentCount = commentsResult.activeCount;
totalCommentCount = commentsResult.totalCount;
fileChanges = fileChangesResult.fileChanges;
fileChangesSummary = fileChangesResult.summary;
}
catch (error) {
console.error('Failed to fetch additional PR data:', error);
}
const formattedResponse = this.apiClient.getIsServer()
? formatServerResponse(pr, mergeInfo, this.baseUrl)
: formatCloudResponse(pr);
const enhancedResponse = {
...formattedResponse,
active_comments: comments,
active_comment_count: activeCommentCount,
total_comment_count: totalCommentCount,
file_changes: fileChanges,
file_changes_summary: fileChangesSummary
};
return {
content: [
{
type: 'text',
text: JSON.stringify(enhancedResponse, null, 2),
},
],
};
}
catch (error) {
return this.apiClient.handleApiError(error, `getting pull request ${pull_request_id} in ${workspace}/${repository}`);
}
}
async handleListPullRequests(args) {
if (!isListPullRequestsArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for list_pull_requests');
}
const { workspace, repository, state = 'OPEN', author, reviewer, limit = 25, start = 0, fields } = args;
try {
let apiPath;
let params = {};
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
params = {
state: state === 'ALL' ? undefined : state,
limit,
start,
};
// Handle participant filters according to Bitbucket Server API
let filterIndex = 1;
if (author) {
params[`username.${filterIndex}`] = author;
params[`role.${filterIndex}`] = 'AUTHOR';
filterIndex++;
}
if (reviewer) {
params[`username.${filterIndex}`] = reviewer;
params[`role.${filterIndex}`] = 'REVIEWER';
filterIndex++;
}
}
else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
params = {
state: state === 'ALL' ? undefined : state,
pagelen: limit,
page: Math.floor(start / limit) + 1,
};
// Build query string for Cloud API
let queryParts = [];
if (author) {
queryParts.push(`author.username="${author}"`);
}
if (reviewer) {
queryParts.push(`reviewers.username="${reviewer}"`);
}
if (queryParts.length > 0) {
params['q'] = queryParts.join(' AND ');
}
}
const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params });
let pullRequests = [];
let totalCount = 0;
let nextPageStart = null;
if (this.apiClient.getIsServer()) {
pullRequests = (response.values || []).map((pr) => formatServerResponse(pr, undefined, this.baseUrl));
totalCount = response.size || 0;
if (!response.isLastPage && response.nextPageStart !== undefined) {
nextPageStart = response.nextPageStart;
}
}
else {
pullRequests = (response.values || []).map((pr) => formatCloudResponse(pr));
totalCount = response.size || 0;
if (response.next) {
nextPageStart = start + limit;
}
}
// Apply field filtering if specified
const filteredPullRequests = applyFieldsFilter(pullRequests, fields);
return {
content: [
{
type: 'text',
text: JSON.stringify({
pull_requests: filteredPullRequests,
total_count: totalCount,
start,
limit,
has_more: nextPageStart !== null,
next_start: nextPageStart,
}, null, 2),
},
],
};
}
catch (error) {
return this.apiClient.handleApiError(error, `listing pull requests in ${workspace}/${repository}`);
}
}
async handleCreatePullRequest(args) {
if (!isCreatePullRequestArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for create_pull_request');
}
const { workspace, repository, title, source_branch, destination_branch, description, reviewers, close_source_branch } = args;
try {
let apiPath;
let requestBody;
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
requestBody = {
title,
description: description || '',
fromRef: {
id: `refs/heads/${source_branch}`,
repository: {
slug: repository,
project: {
key: workspace
}
}
},
toRef: {
id: `refs/heads/${destination_branch}`,
repository: {
slug: repository,
project: {
key: workspace
}
}
},
reviewers: reviewers?.map(r => ({ user: { name: r } })) || []
};
}
else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
requestBody = {
title,
description: description || '',
source: {
branch: {
name: source_branch
}
},
destination: {
branch: {
name: destination_branch
}
},
close_source_branch: close_source_branch || false,
reviewers: reviewers?.map(r => ({ username: r })) || []
};
}
const pr = await this.apiClient.makeRequest('post', apiPath, requestBody);
const formattedResponse = this.apiClient.getIsServer()
? formatServerResponse(pr, undefined, this.baseUrl)
: formatCloudResponse(pr);
return {
content: [
{
type: 'text',
text: JSON.stringify({
message: 'Pull request created successfully',
pull_request: formattedResponse
}, null, 2),
},
],
};
}
catch (error) {
return this.apiClient.handleApiError(error, `creating pull request in ${workspace}/${repository}`);
}
}
async handleUpdatePullRequest(args) {
if (!isUpdatePullRequestArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for update_pull_request');
}
const { workspace, repository, pull_request_id, title, description, destination_branch, reviewers } = args;
try {
let apiPath;
let requestBody = {};
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
// First get the current PR to get version number and existing data
const currentPr = await this.apiClient.makeRequest('get', apiPath);
requestBody.version = currentPr.version;
if (title !== undefined)
requestBody.title = title;
if (description !== undefined)
requestBody.description = description;
if (destination_branch !== undefined) {
requestBody.toRef = {
id: `refs/heads/${destination_branch}`,
repository: {
slug: repository,
project: {
key: workspace
}
}
};
}
// Handle reviewers: preserve existing ones if not explicitly updating
if (reviewers !== undefined) {
// User wants to update reviewers
// Create a map of existing reviewers for preservation of approval status
const existingReviewersMap = new Map(currentPr.reviewers.map((r) => [r.user.name, r]));
requestBody.reviewers = reviewers.map(username => {
const existing = existingReviewersMap.get(username);
if (existing) {
// Preserve existing reviewer's full data including approval status
return existing;
}
else {
// Add new reviewer (without approval status)
return { user: { name: username } };
}
});
}
else {
// No reviewers provided - preserve existing reviewers with their full data
requestBody.reviewers = currentPr.reviewers;
}
}
else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
if (title !== undefined)
requestBody.title = title;
if (description !== undefined)
requestBody.description = description;
if (destination_branch !== undefined) {
requestBody.destination = {
branch: {
name: destination_branch
}
};
}
if (reviewers !== undefined) {
requestBody.reviewers = reviewers.map(r => ({ username: r }));
}
}
const pr = await this.apiClient.makeRequest('put', apiPath, requestBody);
const formattedResponse = this.apiClient.getIsServer()
? formatServerResponse(pr, undefined, this.baseUrl)
: formatCloudResponse(pr);
return {
content: [
{
type: 'text',
text: JSON.stringify({
message: 'Pull request updated successfully',
pull_request: formattedResponse
}, null, 2),
},
],
};
}
catch (error) {
return this.apiClient.handleApiError(error, `updating pull request ${pull_request_id} in ${workspace}/${repository}`);
}
}
async handleAddComment(args) {
if (!isAddCommentArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for add_comment');
}
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;
let sequentialPosition;
if (code_snippet && !line_number && file_path) {
try {
const resolved = await this.resolveLineFromCode(workspace, repository, pull_request_id, file_path, code_snippet, search_context, match_strategy);
line_number = resolved.line_number;
line_type = resolved.line_type;
sequentialPosition = resolved.sequential_position;
}
catch (error) {
throw error;
}
}
if (suggestion && (!file_path || !line_number)) {
throw new McpError(ErrorCode.InvalidParams, 'Suggestions require file_path and line_number to be specified');
}
const isInlineComment = file_path !== undefined && line_number !== undefined;
let finalCommentText = comment_text;
if (suggestion) {
finalCommentText = formatSuggestionComment(comment_text, suggestion, line_number, suggestion_end_line || line_number);
}
try {
let apiPath;
let requestBody;
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/comments`;
requestBody = {
text: finalCommentText
};
if (parent_comment_id !== undefined) {
requestBody.parent = { id: parent_comment_id };
}
if (isInlineComment) {
requestBody.anchor = {
line: line_number,
lineType: line_type || 'CONTEXT',
fileType: line_type === 'REMOVED' ? 'FROM' : 'TO',
path: file_path,
diffType: 'EFFECTIVE'
};
}
}
else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/comments`;
requestBody = {
content: {
raw: finalCommentText
}
};
if (parent_comment_id !== undefined) {
requestBody.parent = { id: parent_comment_id };
}
if (isInlineComment) {
requestBody.inline = {
to: line_number,
path: file_path
};
}
}
const comment = await this.apiClient.makeRequest('post', apiPath, requestBody);
const responseMessage = suggestion
? 'Comment with code suggestion added successfully'
: (isInlineComment ? 'Inline comment added successfully' : 'Comment added successfully');
return {
content: [
{
type: 'text',
text: JSON.stringify({
message: responseMessage,
comment: {
id: comment.id,
text: this.apiClient.getIsServer() ? comment.text : comment.content.raw,
author: this.apiClient.getIsServer() ? comment.author.displayName : comment.user.display_name,
created_on: this.apiClient.getIsServer() ? new Date(comment.createdDate).toLocaleString() : comment.created_on,
file_path: isInlineComment ? file_path : undefined,
line_number: isInlineComment ? line_number : undefined,
line_type: isInlineComment ? (line_type || 'CONTEXT') : undefined,
has_suggestion: !!suggestion,
suggestion_lines: suggestion ? (suggestion_end_line ? `${line_number}-${suggestion_end_line}` : `${line_number}`) : undefined
}
}, null, 2),
},
],
};
}
catch (error) {
return this.apiClient.handleApiError(error, `adding ${isInlineComment ? 'inline ' : ''}comment to pull request ${pull_request_id} in ${workspace}/${repository}`);
}
}
async handleMergePullRequest(args) {
if (!isMergePullRequestArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for merge_pull_request');
}
const { workspace, repository, pull_request_id, merge_strategy, close_source_branch, commit_message } = args;
try {
let apiPath;
let requestBody = {};
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/merge`;
// Get current PR version
const prPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
const currentPr = await this.apiClient.makeRequest('get', prPath);
requestBody.version = currentPr.version;
if (commit_message) {
requestBody.message = commit_message;
}
}
else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/merge`;
if (merge_strategy) {
requestBody.merge_strategy = merge_strategy;
}
if (close_source_branch !== undefined) {
requestBody.close_source_branch = close_source_branch;
}
if (commit_message) {
requestBody.message = commit_message;
}
}
const result = await this.apiClient.makeRequest('post', apiPath, requestBody);
return {
content: [
{
type: 'text',
text: JSON.stringify({
message: 'Pull request merged successfully',
merge_commit: this.apiClient.getIsServer() ? result.properties?.mergeCommit : result.merge_commit?.hash,
pull_request_id
}, null, 2),
},
],
};
}
catch (error) {
return this.apiClient.handleApiError(error, `merging pull request ${pull_request_id} in ${workspace}/${repository}`);
}
}
async fetchPullRequestComments(workspace, repository, pullRequestId) {
try {
let comments = [];
let activeCount = 0;
let totalCount = 0;
if (this.apiClient.getIsServer()) {
const processNestedComments = (comment, anchor) => {
const formattedComment = {
id: comment.id,
author: comment.author.displayName,
text: comment.text,
created_on: new Date(comment.createdDate).toISOString(),
is_inline: !!anchor,
file_path: anchor?.path,
line_number: anchor?.line,
state: comment.state
};
if (comment.comments && comment.comments.length > 0) {
formattedComment.replies = comment.comments
.filter((reply) => {
if (reply.state === 'RESOLVED')
return false;
if (anchor && anchor.orphaned === true)
return false;
return true;
})
.map((reply) => processNestedComments(reply, anchor));
}
return formattedComment;
};
const countAllComments = (comment) => {
let count = 1;
if (comment.comments && comment.comments.length > 0) {
count += comment.comments.reduce((sum, reply) => sum + countAllComments(reply), 0);
}
return count;
};
const countActiveComments = (comment, anchor) => {
let count = 0;
if (comment.state !== 'RESOLVED' && (!anchor || anchor.orphaned !== true)) {
count = 1;
}
if (comment.comments && comment.comments.length > 0) {
count += comment.comments.reduce((sum, reply) => sum + countActiveComments(reply, anchor), 0);
}
return count;
};
const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/activities`;
const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
params: { limit: 1000 }
});
const activities = response.values || [];
const commentActivities = activities.filter((a) => a.action === 'COMMENTED' && a.comment);
totalCount = commentActivities.reduce((sum, activity) => {
return sum + countAllComments(activity.comment);
}, 0);
activeCount = commentActivities.reduce((sum, activity) => {
return sum + countActiveComments(activity.comment, activity.commentAnchor);
}, 0);
const processedComments = commentActivities
.filter((a) => {
const c = a.comment;
const anchor = a.commentAnchor;
if (c.state === 'RESOLVED')
return false;
if (anchor && anchor.orphaned === true)
return false;
return true;
})
.map((a) => processNestedComments(a.comment, a.commentAnchor));
comments = processedComments.slice(0, 20);
}
else {
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/comments`;
const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
params: { pagelen: 100 }
});
const allComments = response.values || [];
totalCount = allComments.length;
const activeComments = allComments
.filter((c) => !c.deleted && !c.resolved)
.slice(0, 20);
activeCount = allComments.filter((c) => !c.deleted && !c.resolved).length;
comments = activeComments.map((c) => ({
id: c.id,
author: c.user.display_name,
text: c.content.raw,
created_on: c.created_on,
is_inline: !!c.inline,
file_path: c.inline?.path,
line_number: c.inline?.to
}));
}
return { comments, activeCount, totalCount };
}
catch (error) {
console.error('Failed to fetch comments:', error);
return { comments: [], activeCount: 0, totalCount: 0 };
}
}
async fetchPullRequestFileChanges(workspace, repository, pullRequestId) {
try {
let fileChanges = [];
let totalLinesAdded = 0;
let totalLinesRemoved = 0;
if (this.apiClient.getIsServer()) {
const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/changes`;
const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
params: { limit: 1000 }
});
const changes = response.values || [];
fileChanges = changes.map((change) => {
let status = 'modified';
if (change.type === 'ADD')
status = 'added';
else if (change.type === 'DELETE')
status = 'removed';
else if (change.type === 'MOVE' || change.type === 'RENAME')
status = 'renamed';
return {
path: change.path.toString,
status,
old_path: change.srcPath?.toString
};
});
}
else {
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diffstat`;
const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
params: { pagelen: 100 }
});
const diffstats = response.values || [];
fileChanges = diffstats.map((stat) => {
totalLinesAdded += stat.lines_added;
totalLinesRemoved += stat.lines_removed;
return {
path: stat.path,
status: stat.type,
old_path: stat.old?.path
};
});
}
const summary = {
total_files: fileChanges.length
};
return { fileChanges, summary };
}
catch (error) {
console.error('Failed to fetch file changes:', error);
return {
fileChanges: [],
summary: {
total_files: 0
}
};
}
}
async resolveLineFromCode(workspace, repository, pullRequestId, filePath, codeSnippet, searchContext, matchStrategy = 'strict') {
try {
const diffContent = await this.getFilteredPullRequestDiff(workspace, repository, pullRequestId, filePath);
const parser = new DiffParser();
const sections = parser.parseDiffIntoSections(diffContent);
let fileSection = sections[0];
if (!this.apiClient.getIsServer()) {
fileSection = sections.find(s => s.filePath === filePath) || sections[0];
}
if (!fileSection) {
throw new McpError(ErrorCode.InvalidParams, `File ${filePath} not found in pull request diff`);
}
const matches = this.findCodeMatches(fileSection.content, codeSnippet, searchContext);
if (matches.length === 0) {
throw new McpError(ErrorCode.InvalidParams, `Code snippet not found in ${filePath}`);
}
if (matches.length === 1) {
return {
line_number: matches[0].line_number,
line_type: matches[0].line_type,
sequential_position: matches[0].sequential_position,
hunk_info: matches[0].hunk_info,
diff_context: matches[0].preview,
diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
calculation_details: `Direct line number from diff: ${matches[0].line_number}`
};
}
if (matchStrategy === 'best') {
const best = this.selectBestMatch(matches);
return {
line_number: best.line_number,
line_type: best.line_type,
sequential_position: best.sequential_position,
hunk_info: best.hunk_info,
diff_context: best.preview,
diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
calculation_details: `Best match selected from ${matches.length} matches, line: ${best.line_number}`
};
}
const error = {
code: 'MULTIPLE_MATCHES_FOUND',
message: `Code snippet '${codeSnippet.substring(0, 50)}...' found in ${matches.length} locations`,
occurrences: matches.map(m => ({
line_number: m.line_number,
file_path: filePath,
preview: m.preview,
confidence: m.confidence,
line_type: m.line_type
})),
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'
};
throw new McpError(ErrorCode.InvalidParams, JSON.stringify({ error }));
}
catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Failed to resolve line from code: ${error instanceof Error ? error.message : String(error)}`);
}
}
findCodeMatches(diffContent, codeSnippet, searchContext) {
const lines = diffContent.split('\n');
const matches = [];
let currentDestLine = 0; // Destination file line number
let currentSrcLine = 0; // Source file line number
let inHunk = false;
let sequentialAddedCount = 0; // Track sequential ADDED lines
let currentHunkIndex = -1;
let currentHunkDestStart = 0;
let currentHunkSrcStart = 0;
let destPositionInHunk = 0; // Track position in destination file relative to hunk start
let srcPositionInHunk = 0; // Track position in source file relative to hunk start
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('@@')) {
const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
if (match) {
currentHunkSrcStart = parseInt(match[1]);
currentHunkDestStart = parseInt(match[2]);
currentSrcLine = currentHunkSrcStart;
currentDestLine = currentHunkDestStart;
inHunk = true;
currentHunkIndex++;
destPositionInHunk = 0;
srcPositionInHunk = 0;
continue;
}
}
if (!inHunk)
continue;
if (line === '') {
inHunk = false;
continue;
}
let lineType;
let lineContent = '';
let lineNumber = 0;
if (line.startsWith('+')) {
lineType = 'ADDED';
lineContent = line.substring(1);
lineNumber = currentHunkDestStart + destPositionInHunk;
destPositionInHunk++;
sequentialAddedCount++;
}
else if (line.startsWith('-')) {
lineType = 'REMOVED';
lineContent = line.substring(1);
lineNumber = currentHunkSrcStart + srcPositionInHunk;
srcPositionInHunk++;
}
else if (line.startsWith(' ')) {
lineType = 'CONTEXT';
lineContent = line.substring(1);
lineNumber = currentHunkDestStart + destPositionInHunk;
destPositionInHunk++;
srcPositionInHunk++;
}
else {
inHunk = false;
continue;
}
if (lineContent.trim() === codeSnippet.trim()) {
const confidence = this.calculateConfidence(lines, i, searchContext, lineType);
matches.push({
line_number: lineNumber,
line_type: lineType,
exact_content: codeSnippet,
preview: this.getPreview(lines, i),
confidence,
context: this.extractContext(lines, i),
sequential_position: lineType === 'ADDED' ? sequentialAddedCount : undefined,
hunk_info: {
hunk_index: currentHunkIndex,
destination_start: currentHunkDestStart,
line_in_hunk: destPositionInHunk
}
});
}
if (lineType === 'ADDED') {
currentDestLine++;
}
else if (lineType === 'REMOVED') {
currentSrcLine++;
}
else if (lineType === 'CONTEXT') {
currentSrcLine++;
currentDestLine++;
}
}
return matches;
}
calculateConfidence(lines, index, searchContext, lineType) {
let confidence = 0.5; // Base confidence
if (!searchContext) {
return confidence;
}
if (searchContext.before) {
let matchedBefore = 0;
for (let j = 0; j < searchContext.before.length; j++) {
const contextLine = searchContext.before[searchContext.before.length - 1 - j];
const checkIndex = index - j - 1;
if (checkIndex >= 0) {
const checkLine = lines[checkIndex].substring(1);
if (checkLine.trim() === contextLine.trim()) {
matchedBefore++;
}
}
}
confidence += (matchedBefore / searchContext.before.length) * 0.3;
}
if (searchContext.after) {
let matchedAfter = 0;
for (let j = 0; j < searchContext.after.length; j++) {
const contextLine = searchContext.after[j];
const checkIndex = index + j + 1;
if (checkIndex < lines.length) {
const checkLine = lines[checkIndex].substring(1);
if (checkLine.trim() === contextLine.trim()) {
matchedAfter++;
}
}
}
confidence += (matchedAfter / searchContext.after.length) * 0.3;
}
if (lineType === 'ADDED') {
confidence += 0.1;
}
return Math.min(confidence, 1.0);
}
getPreview(lines, index) {
const start = Math.max(0, index - 1);
const end = Math.min(lines.length, index + 2);
const previewLines = [];
for (let i = start; i < end; i++) {
const prefix = i === index ? '> ' : ' ';
previewLines.push(prefix + lines[i]);
}
return previewLines.join('\n');
}
extractContext(lines, index) {
const linesBefore = [];
const linesAfter = [];
for (let i = Math.max(0, index - 2); i < index; i++) {
if (lines[i].match(/^[+\- ]/)) {
linesBefore.push(lines[i].substring(1));
}
}
for (let i = index + 1; i < Math.min(lines.length, index + 3); i++) {
if (lines[i].match(/^[+\- ]/)) {
linesAfter.push(lines[i].substring(1));
}
}
return {
lines_before: linesBefore,
lines_after: linesAfter
};
}
selectBestMatch(matches) {
return matches.sort((a, b) => b.confidence - a.confidence)[0];
}
async handleListPrCommits(args) {
if (!isListPrCommitsArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for list_pr_commits');
}
const { workspace, repository, pull_request_id, limit = 25, start = 0 } = args;
try {
// First get the PR details to include in response
const prPath = this.apiClient.getIsServer()
? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
: `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
let prTitle = '';
try {
const pr = await this.apiClient.makeRequest('get', prPath);
prTitle = pr.title;
}
catch (e) {
// Ignore error, PR title is optional
}
let apiPath;
let params = {};
let commits = [];
let totalCount = 0;
let nextPageStart = null;
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/commits`;
params = {
limit,
start,
withCounts: true
};
const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params });
// Format commits
commits = (response.values || []).map((commit) => formatServerCommit(commit));
totalCount = response.size || commits.length;
if (!response.isLastPage && response.nextPageStart !== undefined) {
nextPageStart = response.nextPageStart;
}
}
else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/commits`;
params = {
pagelen: limit,
page: Math.floor(start / limit) + 1
};
const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params });
// Format commits
commits = (response.values || []).map((commit) => formatCloudCommit(commit));
totalCount = response.size || commits.length;
if (response.next) {
nextPageStart = start + limit;
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
pull_request_id,
pull_request_title: prTitle,
commits,
total_count: totalCount,
start,
limit,
has_more: nextPageStart !== null,
next_start: nextPageStart
}, null, 2),
},
],
};
}
catch (error) {
return this.apiClient.handleApiError(error, `listing commits for pull request ${pull_request_id} in ${workspace}/${repository}`);
}
}
}
//# sourceMappingURL=pull-request-handlers.js.map