rafee-bitbucket-mcp-server
Version:
MCP server for Bitbucket API integration - supports both Cloud and Server
1,044 lines • 47.8 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 { isGetPullRequestArgs, isListPullRequestsArgs, isCreatePullRequestArgs, isUpdatePullRequestArgs, isAddCommentArgs, isMergePullRequestArgs, isListPrCommitsArgs, isGetDefaultReviewersArgs, } from "../types/guards.js";
export class PullRequestHandlers {
apiClient;
userHandlers;
username;
baseUrl;
constructor(apiClient, userHandlers, username, baseUrl) {
this.apiClient = apiClient;
this.userHandlers = userHandlers;
this.username = username;
this.baseUrl = baseUrl;
}
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, limit = 25, start = 0, } = 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,
};
if (author) {
params["role.1"] = "AUTHOR";
params["username.1"] = author;
}
}
else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
params = {
state: state === "ALL" ? undefined : state,
pagelen: limit,
page: Math.floor(start / limit) + 1,
};
if (author) {
params["q"] = `author.username="${author}"`;
}
}
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;
}
}
return {
content: [
{
type: "text",
text: JSON.stringify({
pull_requests: pullRequests,
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;
let allReviewers = [];
if (reviewers) {
allReviewers = reviewers;
}
const defaultReviewersResponse = await this.getDefaultReviewers({
workspace,
repository,
});
let defaultReviewers = JSON.parse(defaultReviewersResponse.content[0].text)?.reviewers ?? [];
const currentUser = await this.userHandlers.getCurrentUser();
const currentUserAccountId = currentUser.account_id;
defaultReviewers = defaultReviewers.filter((r) => r.account_id !== currentUserAccountId);
if (defaultReviewers) {
allReviewers = [
...allReviewers,
...defaultReviewers.map((r) => ({
type: r.type,
uuid: r.uuid,
})),
];
}
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: allReviewers?.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: allReviewers,
...(args.rendered && {
rendered: {
description: args.rendered.description,
},
}),
};
}
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 (args.rendered && args.rendered.description) {
requestBody.rendered = {
description: args.rendered.description,
};
}
if (destination_branch !== undefined) {
requestBody.destination = {
branch: {
name: destination_branch,
},
};
}
if (reviewers !== undefined) {
requestBody.reviewers = reviewers;
}
}
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}`);
}
}
async getDefaultReviewers(args) {
if (!isGetDefaultReviewersArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, "Invalid arguments for get_default_reviewers");
}
const { workspace, repository } = args;
try {
// TODO: find docs to update
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/default-reviewers`;
const response = await this.apiClient.makeRequest("get", apiPath);
return response.values?.map((r) => r.user.displayName) || [];
}
else {
// Bitbucket Cloud API
const apiPath = `/repositories/${workspace}/${repository}/default-reviewers`;
const response = await this.apiClient.makeRequest("get", apiPath);
return {
content: [
{
type: "text",
text: JSON.stringify({
reviewers: response.values?.map((r) => ({
display_name: r.display_name,
uuid: r.uuid,
type: r.type,
account_id: r.account_id,
})),
}),
},
],
};
}
}
catch (error) {
return this.apiClient.handleApiError(error, `getting default reviewers in ${workspace}/${repository}`);
}
}
}
//# sourceMappingURL=pull-request-handlers.js.map