Skip to main content
Glama

Atlassian Bitbucket MCP Server

by aashari
vendor.atlassian.pullrequests.service.ts24.8 kB
import { createAuthMissingError } from '../utils/error.util.js'; import { Logger } from '../utils/logger.util.js'; import { fetchAtlassian, getAtlassianCredentials, } from '../utils/transport.util.js'; import { PullRequestDetailed, PullRequestsResponse, ListPullRequestsParams, GetPullRequestParams, GetPullRequestCommentsParams, PullRequestCommentsResponse, PullRequestComment, CreatePullRequestCommentParams, CreatePullRequestParams, UpdatePullRequestParams, ApprovePullRequestParams, RejectPullRequestParams, PullRequestParticipant, DiffstatResponse, } from './vendor.atlassian.pullrequests.types.js'; /** * Base API path for Bitbucket REST API v2 * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/ * @constant {string} */ const API_PATH = '/2.0'; /** * @namespace VendorAtlassianPullRequestsService * @description Service for interacting with Bitbucket Pull Requests API. * Provides methods for listing pull requests and retrieving pull request details. * All methods require valid Atlassian credentials configured in the environment. */ // Create a contextualized logger for this file const serviceLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', ); // Log service initialization serviceLogger.debug('Bitbucket pull requests service initialized'); /** * List pull requests for a repository * @param {ListPullRequestsParams} params - Parameters for the request * @param {string} params.workspace - The workspace slug or UUID * @param {string} params.repo_slug - The repository slug or UUID * @param {PullRequestState | PullRequestState[]} [params.state] - Filter by pull request state (default: 'OPEN') * @param {string} [params.q] - Query string to filter pull requests * @param {string} [params.sort] - Property to sort by (e.g., 'created_on', '-updated_on') * @param {number} [params.page] - Page number for pagination * @param {number} [params.pagelen] - Number of items per page * @returns {Promise<PullRequestsResponse>} Response containing pull requests * @example * ```typescript * // List open pull requests in a repository, sorted by creation date * const response = await list({ * workspace: 'myworkspace', * repo_slug: 'myrepo', * sort: '-created_on', * pagelen: 25 * }); * ``` */ async function list( params: ListPullRequestsParams, ): Promise<PullRequestsResponse> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'list', ); methodLogger.debug('Listing Bitbucket pull requests with params:', params); if (!params.workspace || !params.repo_slug) { throw new Error('Both workspace and repo_slug parameters are required'); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } // Construct query parameters const queryParams = new URLSearchParams(); // Add state parameter(s) - default to OPEN if not specified if (params.state) { if (Array.isArray(params.state)) { // For multiple states, repeat the parameter params.state.forEach((state) => { queryParams.append('state', state); }); } else { queryParams.set('state', params.state); } } // Add optional query parameters if (params.q) { queryParams.set('q', params.q); } if (params.sort) { queryParams.set('sort', params.sort); } if (params.pagelen) { queryParams.set('pagelen', params.pagelen.toString()); } if (params.page) { queryParams.set('page', params.page.toString()); } const queryString = queryParams.toString() ? `?${queryParams.toString()}` : ''; const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests${queryString}`; methodLogger.debug(`Sending request to: ${path}`); return fetchAtlassian<PullRequestsResponse>(credentials, path); } /** * Get detailed information about a specific Bitbucket pull request * * Retrieves comprehensive details about a single pull request. * * @async * @memberof VendorAtlassianPullRequestsService * @param {GetPullRequestParams} params - Parameters for the request * @param {string} params.workspace - The workspace slug or UUID * @param {string} params.repo_slug - The repository slug or UUID * @param {number} params.pull_request_id - The ID of the pull request * @returns {Promise<PullRequestDetailed>} Promise containing the detailed pull request information * @throws {Error} If Atlassian credentials are missing or API request fails * @example * // Get pull request details * const pullRequest = await get({ * workspace: 'my-workspace', * repo_slug: 'my-repo', * pull_request_id: 123 * }); */ async function get(params: GetPullRequestParams): Promise<PullRequestDetailed> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'get', ); methodLogger.debug( `Getting Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`, ); // Validate pull_request_id is a positive integer const prId = Number(params.pull_request_id); if (isNaN(prId) || prId <= 0 || !Number.isInteger(prId)) { throw new Error( `Invalid pull request ID: ${params.pull_request_id}. Pull request ID must be a positive integer.`, ); } if (!params.workspace || !params.repo_slug || !params.pull_request_id) { throw new Error( 'workspace, repo_slug, and pull_request_id parameters are all required', ); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}`; methodLogger.debug(`Sending request to: ${path}`); return fetchAtlassian<PullRequestDetailed>(credentials, path); } /** * Get comments for a specific Bitbucket pull request * * Retrieves all comments on a specific pull request, including general comments and * inline code review comments. Supports pagination. * * @async * @memberof VendorAtlassianPullRequestsService * @param {GetPullRequestCommentsParams} params - Parameters for the request * @param {string} params.workspace - The workspace slug or UUID * @param {string} params.repo_slug - The repository slug or UUID * @param {number} params.pull_request_id - The ID of the pull request * @param {number} [params.page] - Page number for pagination * @param {number} [params.pagelen] - Number of items per page * @returns {Promise<PullRequestCommentsResponse>} Promise containing the pull request comments * @throws {Error} If Atlassian credentials are missing or API request fails * @example * // Get comments for a pull request * const comments = await getComments({ * workspace: 'my-workspace', * repo_slug: 'my-repo', * pull_request_id: 123, * pagelen: 25 * }); */ async function getComments( params: GetPullRequestCommentsParams, ): Promise<PullRequestCommentsResponse> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'getComments', ); methodLogger.debug( `Getting comments for Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`, ); // Validate pull_request_id is a positive integer const prId = Number(params.pull_request_id); if (isNaN(prId) || prId <= 0 || !Number.isInteger(prId)) { throw new Error( `Invalid pull request ID: ${params.pull_request_id}. Pull request ID must be a positive integer.`, ); } if (!params.workspace || !params.repo_slug || !params.pull_request_id) { throw new Error( 'workspace, repo_slug, and pull_request_id parameters are all required', ); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } // Build query parameters const queryParams = new URLSearchParams(); // Add pagination parameters if provided if (params.pagelen) { queryParams.set('pagelen', params.pagelen.toString()); } if (params.page) { queryParams.set('page', params.page.toString()); } // Add sort parameter if provided if (params.sort) { queryParams.set('sort', params.sort); } const queryString = queryParams.toString() ? `?${queryParams.toString()}` : ''; const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/comments${queryString}`; methodLogger.debug(`Sending request to: ${path}`); return fetchAtlassian<PullRequestCommentsResponse>(credentials, path); } /** * Add a comment to a specific Bitbucket pull request * * Creates a new comment on a pull request, either as a general comment or * as an inline code comment attached to a specific file and line. * * @async * @memberof VendorAtlassianPullRequestsService * @param {CreatePullRequestCommentParams} params - Parameters for the request * @param {string} params.workspace - The workspace slug or UUID * @param {string} params.repo_slug - The repository slug or UUID * @param {number} params.pull_request_id - The ID of the pull request * @param {Object} params.content - The content of the comment * @param {string} params.content.raw - The raw text of the comment * @param {Object} [params.inline] - Optional inline comment location * @param {string} params.inline.path - The file path for the inline comment * @param {number} params.inline.to - The line number in the file * @returns {Promise<PullRequestComment>} Promise containing the created comment * @throws {Error} If Atlassian credentials are missing or API request fails * @example * // Add a general comment to a pull request * const comment = await addComment({ * workspace: 'my-workspace', * repo_slug: 'my-repo', * pull_request_id: 123, * content: { raw: "This looks good to me!" } * }); * * // Add an inline code comment * const comment = await addComment({ * workspace: 'my-workspace', * repo_slug: 'my-repo', * pull_request_id: 123, * content: { raw: "Consider using a constant here instead." }, * inline: { path: "src/main.js", to: 42 } * }); */ async function createComment( params: CreatePullRequestCommentParams, ): Promise<PullRequestComment> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'createComment', ); methodLogger.debug( `Creating comment on Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`, ); if (!params.workspace || !params.repo_slug || !params.pull_request_id) { throw new Error( 'workspace, repo_slug, and pull_request_id parameters are all required', ); } if (!params.content || !params.content.raw) { throw new Error('Comment content is required'); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/comments`; methodLogger.debug(`Sending POST request to: ${path}`); return fetchAtlassian<PullRequestComment>(credentials, path, { method: 'POST', body: { content: params.content, inline: params.inline, parent: params.parent, }, }); } /** * Create a new pull request * @param {CreatePullRequestParams} params - Parameters for the request * @param {string} params.workspace - The workspace slug or UUID * @param {string} params.repo_slug - The repository slug or UUID * @param {string} params.title - Title of the pull request * @param {string} params.source.branch.name - Source branch name * @param {string} params.destination.branch.name - Destination branch name (defaults to main/master) * @param {string} [params.description] - Optional description for the pull request * @param {boolean} [params.close_source_branch] - Whether to close the source branch after merge (default: false) * @returns {Promise<PullRequestDetailed>} Detailed information about the created pull request * @example * ```typescript * // Create a new pull request * const pullRequest = await create({ * workspace: 'myworkspace', * repo_slug: 'myrepo', * title: 'Add new feature', * source: { * branch: { * name: 'feature/new-feature' * } * }, * destination: { * branch: { * name: 'main' * } * }, * description: 'This PR adds a new feature...', * close_source_branch: true * }); * ``` */ async function create( params: CreatePullRequestParams, ): Promise<PullRequestDetailed> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'create', ); methodLogger.debug( 'Creating new Bitbucket pull request with params:', params, ); if (!params.workspace || !params.repo_slug) { throw new Error('Both workspace and repo_slug parameters are required'); } if (!params.title) { throw new Error('Pull request title is required'); } if (!params.source || !params.source.branch || !params.source.branch.name) { throw new Error('Source branch name is required'); } // Destination branch is required but may have a default if ( !params.destination || !params.destination.branch || !params.destination.branch.name ) { throw new Error('Destination branch name is required'); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests`; // Construct request body with only the fields needed by the API const requestBody = { title: params.title, source: { branch: { name: params.source.branch.name, }, }, destination: { branch: { name: params.destination.branch.name, }, }, description: params.description || '', close_source_branch: !!params.close_source_branch, }; methodLogger.debug(`Sending POST request to: ${path}`); return fetchAtlassian<PullRequestDetailed>(credentials, path, { method: 'POST', body: requestBody, }); } /** * Get raw diff content for a specific Bitbucket pull request * * Retrieves the raw diff content showing actual code changes in the pull request. * The diff is returned in a standard unified diff format. * * @async * @memberof VendorAtlassianPullRequestsService * @param {GetPullRequestParams} params - Parameters for the request * @param {string} params.workspace - The workspace slug or UUID * @param {string} params.repo_slug - The repository slug or UUID * @param {number} params.pull_request_id - The ID of the pull request * @returns {Promise<string>} Promise containing the raw diff content * @throws {Error} If Atlassian credentials are missing or API request fails * @example * // Get raw diff content for a pull request * const diffContent = await getRawDiff({ * workspace: 'my-workspace', * repo_slug: 'my-repo', * pull_request_id: 123 * }); */ async function getRawDiff(params: GetPullRequestParams): Promise<string> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'getRawDiff', ); methodLogger.debug( `Getting raw diff for Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`, ); if (!params.workspace || !params.repo_slug || !params.pull_request_id) { throw new Error( 'workspace, repo_slug, and pull_request_id parameters are all required', ); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } // Use the diff endpoint directly const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/diff`; methodLogger.debug(`Sending request to: ${path}`); // Override the Accept header to get raw diff content instead of JSON // The transport utility handles the text response automatically return fetchAtlassian<string>(credentials, path, { headers: { Accept: 'text/plain', }, }); } /** * Get the diffstat information for a pull request * * Returns summary statistics about the changes in a pull request, * including files changed, insertions, and deletions. * * @async * @memberof VendorAtlassianPullRequestsService * @param {GetPullRequestParams} params - Parameters for the request * @returns {Promise<DiffstatResponse>} Promise containing the diffstat response */ async function getDiffstat( params: GetPullRequestParams, ): Promise<DiffstatResponse> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'getDiffstat', ); methodLogger.debug( `Getting diffstat for Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`, ); if (!params.workspace || !params.repo_slug || !params.pull_request_id) { throw new Error( 'workspace, repo_slug, and pull_request_id parameters are all required', ); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/diffstat`; methodLogger.debug(`Sending request to: ${path}`); return fetchAtlassian<DiffstatResponse>(credentials, path); } /** * Fetches the raw diff content from a specific Bitbucket API URL. * Used primarily for fetching file-specific diffs linked from inline comments. * * @async * @param {string} url - The exact Bitbucket API URL to fetch the diff from. * @returns {Promise<string>} Promise containing the raw diff content as a string. * @throws {Error} If Atlassian credentials are missing or the API request fails. */ async function getDiffForUrl(url: string): Promise<string> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'getDiffForUrl', ); methodLogger.debug(`Getting raw diff from URL: ${url}`); if (!url) { throw new Error('URL parameter is required'); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } // Parse the provided URL to extract the path and query string let urlPath = ''; try { const parsedUrl = new URL(url); urlPath = parsedUrl.pathname + parsedUrl.search; methodLogger.debug(`Parsed path and query from URL: ${urlPath}`); } catch (parseError) { methodLogger.error(`Failed to parse URL: ${url}`, parseError); throw new Error(`Invalid URL provided for diff fetching: ${url}`); } // Pass only the path part to fetchAtlassian // Ensure the Accept header requests plain text for the raw diff. return fetchAtlassian<string>(credentials, urlPath, { headers: { Accept: 'text/plain', }, }); } /** * Update an existing pull request * @param {UpdatePullRequestParams} params - Parameters for the request * @param {string} params.workspace - The workspace slug or UUID * @param {string} params.repo_slug - The repository slug or UUID * @param {number} params.pull_request_id - The ID of the pull request to update * @param {string} [params.title] - Updated title of the pull request * @param {string} [params.description] - Updated description for the pull request * @returns {Promise<PullRequestDetailed>} Updated pull request information * @example * ```typescript * // Update a pull request's title and description * const updatedPR = await update({ * workspace: 'myworkspace', * repo_slug: 'myrepo', * pull_request_id: 123, * title: 'Updated Feature Implementation', * description: 'This PR now includes additional improvements...' * }); * ``` */ async function update( params: UpdatePullRequestParams, ): Promise<PullRequestDetailed> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'update', ); methodLogger.debug('Updating Bitbucket pull request with params:', params); if (!params.workspace || !params.repo_slug || !params.pull_request_id) { throw new Error( 'workspace, repo_slug, and pull_request_id parameters are all required', ); } // At least one field to update must be provided if (!params.title && !params.description) { throw new Error( 'At least one field to update (title or description) must be provided', ); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}`; // Construct request body with only the fields to update const requestBody: Record<string, unknown> = {}; if (params.title !== undefined) { requestBody.title = params.title; } if (params.description !== undefined) { requestBody.description = params.description; } methodLogger.debug(`Sending PUT request to: ${path}`); return fetchAtlassian<PullRequestDetailed>(credentials, path, { method: 'PUT', body: requestBody, }); } /** * Approve a pull request * @param {ApprovePullRequestParams} params - Parameters for the request * @param {string} params.workspace - The workspace slug or UUID * @param {string} params.repo_slug - The repository slug or UUID * @param {number} params.pull_request_id - The ID of the pull request to approve * @returns {Promise<PullRequestParticipant>} Participant information showing approval status * @example * ```typescript * // Approve a pull request * const approval = await approve({ * workspace: 'myworkspace', * repo_slug: 'myrepo', * pull_request_id: 123 * }); * ``` */ async function approve( params: ApprovePullRequestParams, ): Promise<PullRequestParticipant> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'approve', ); methodLogger.debug( `Approving Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`, ); if (!params.workspace || !params.repo_slug || !params.pull_request_id) { throw new Error( 'workspace, repo_slug, and pull_request_id parameters are all required', ); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/approve`; methodLogger.debug(`Sending POST request to: ${path}`); return fetchAtlassian<PullRequestParticipant>(credentials, path, { method: 'POST', }); } /** * Request changes on a pull request (reject) * @param {RejectPullRequestParams} params - Parameters for the request * @param {string} params.workspace - The workspace slug or UUID * @param {string} params.repo_slug - The repository slug or UUID * @param {number} params.pull_request_id - The ID of the pull request to request changes on * @returns {Promise<PullRequestParticipant>} Participant information showing rejection status * @example * ```typescript * // Request changes on a pull request * const rejection = await reject({ * workspace: 'myworkspace', * repo_slug: 'myrepo', * pull_request_id: 123 * }); * ``` */ async function reject( params: RejectPullRequestParams, ): Promise<PullRequestParticipant> { const methodLogger = Logger.forContext( 'services/vendor.atlassian.pullrequests.service.ts', 'reject', ); methodLogger.debug( `Requesting changes on Bitbucket pull request: ${params.workspace}/${params.repo_slug}/${params.pull_request_id}`, ); if (!params.workspace || !params.repo_slug || !params.pull_request_id) { throw new Error( 'workspace, repo_slug, and pull_request_id parameters are all required', ); } const credentials = getAtlassianCredentials(); if (!credentials) { throw createAuthMissingError( 'Atlassian credentials are required for this operation', ); } const path = `${API_PATH}/repositories/${params.workspace}/${params.repo_slug}/pullrequests/${params.pull_request_id}/request-changes`; methodLogger.debug(`Sending POST request to: ${path}`); return fetchAtlassian<PullRequestParticipant>(credentials, path, { method: 'POST', }); } export default { list, get, getComments, createComment, create, update, approve, reject, getRawDiff, getDiffstat, getDiffForUrl, };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aashari/mcp-server-atlassian-bitbucket'

If you have feedback or need assistance with the MCP directory API, please join our Discord server