UNPKG

rafee-bitbucket-mcp-server

Version:

MCP server for Bitbucket API integration - supports both Cloud and Server

1,044 lines 47.8 kB
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