Skip to main content
Glama

Bitbucket MCP

by MatanYemini
index.ts127 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios, { AxiosInstance, AxiosError } from "axios"; import winston from "winston"; import os from "os"; import path from "path"; import fs from "fs"; // =========== LOGGER SETUP ========== // File-based logging with sensible defaults and ability to disable function getDefaultLogDirectory(): string { if (process.platform === "win32") { const base = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"); return path.join(base, "bitbucket-mcp"); } if (process.platform === "darwin") { return path.join(os.homedir(), "Library", "Logs", "bitbucket-mcp"); } const xdgStateHome = process.env.XDG_STATE_HOME; if (xdgStateHome && xdgStateHome.length > 0) { return path.join(xdgStateHome, "bitbucket-mcp"); } return path.join(os.homedir(), ".local", "state", "bitbucket-mcp"); } function isTruthyEnv(value: unknown): boolean { if (value === undefined || value === null) return false; const normalized = String(value).toLowerCase(); return ["1", "true", "yes", "on"].includes(normalized); } function getLogFilePath(): string | undefined { if (isTruthyEnv(process.env.BITBUCKET_LOG_DISABLE)) { return undefined; } const explicitFile = process.env.BITBUCKET_LOG_FILE; if (explicitFile && explicitFile.trim().length > 0) { return explicitFile; } const baseDir = process.env.BITBUCKET_LOG_DIR && process.env.BITBUCKET_LOG_DIR.trim().length > 0 ? process.env.BITBUCKET_LOG_DIR! : getDefaultLogDirectory(); let effectiveDir = baseDir as string; if (isTruthyEnv(process.env.BITBUCKET_LOG_PER_CWD)) { const sanitizedCwd = process .cwd() .replace(/[\\/]/g, "_") .replace(/[:*?"<>|]/g, ""); effectiveDir = path.join(baseDir as string, sanitizedCwd); } try { fs.mkdirSync(effectiveDir, { recursive: true }); } catch { return undefined; // If we cannot create the directory, disable file logging rather than polluting CWD } return path.join(effectiveDir, "bitbucket.log"); } const resolvedLogFile = getLogFilePath(); const logger = winston.createLogger({ level: "info", format: winston.format.json(), transports: resolvedLogFile ? [new winston.transports.File({ filename: resolvedLogFile })] : [], }); // =========== TYPE DEFINITIONS =========== /** * Represents a Bitbucket repository */ interface BitbucketRepository { uuid: string; name: string; full_name: string; description: string; is_private: boolean; created_on: string; updated_on: string; size: number; language: string; has_issues: boolean; has_wiki: boolean; fork_policy: string; owner: BitbucketAccount; workspace: BitbucketWorkspace; project: BitbucketProject; mainbranch?: BitbucketBranch; website?: string; scm: string; links: Record<string, BitbucketLink[]>; } /** * Represents a Bitbucket account (user or team) */ interface BitbucketAccount { uuid: string; display_name: string; account_id: string; nickname?: string; type: "user" | "team"; links: Record<string, BitbucketLink[]>; } /** * Represents a Bitbucket workspace */ interface BitbucketWorkspace { uuid: string; name: string; slug: string; type: "workspace"; links: Record<string, BitbucketLink[]>; } /** * Represents a Bitbucket project */ interface BitbucketProject { uuid: string; key: string; name: string; description?: string; is_private: boolean; type: "project"; links: Record<string, BitbucketLink[]>; } /** * Represents a Bitbucket branch reference */ interface BitbucketBranch { name: string; type: "branch"; } /** * Represents a hyperlink in Bitbucket API responses */ interface BitbucketLink { href: string; name?: string; } /** * Represents a Bitbucket pull request */ interface BitbucketPullRequest { id: number; title: string; description: string; state: "OPEN" | "MERGED" | "DECLINED" | "SUPERSEDED"; author: BitbucketAccount; source: BitbucketBranchReference; destination: BitbucketBranchReference; created_on: string; updated_on: string; closed_on?: string; comment_count: number; task_count: number; close_source_branch: boolean; reviewers: BitbucketAccount[]; participants: BitbucketParticipant[]; links: Record<string, BitbucketLink[]>; summary?: { raw: string; markup: string; html: string; }; } /** * Represents a branch reference in a pull request */ interface BitbucketBranchReference { branch: { name: string; }; commit: { hash: string; }; repository: BitbucketRepository; } /** * Represents a participant in a pull request */ interface BitbucketParticipant { user: BitbucketAccount; role: "PARTICIPANT" | "REVIEWER"; approved: boolean; state?: "approved" | "changes_requested" | null; participated_on: string; } /** * Represents inline comment positioning information */ interface InlineCommentInline { path: string; from?: number; to?: number; } /** * Represents a Bitbucket branching model */ interface BitbucketBranchingModel { type: "branching_model"; development: { name: string; branch?: BitbucketBranch; use_mainbranch: boolean; }; production?: { name: string; branch?: BitbucketBranch; use_mainbranch: boolean; }; branch_types: Array<{ kind: string; prefix: string; }>; links: Record<string, BitbucketLink[]>; } /** * Represents a Bitbucket branching model settings */ interface BitbucketBranchingModelSettings { type: "branching_model_settings"; development: { name: string; use_mainbranch: boolean; is_valid?: boolean; }; production: { name: string; use_mainbranch: boolean; enabled: boolean; is_valid?: boolean; }; branch_types: Array<{ kind: string; prefix: string; enabled: boolean; }>; links: Record<string, BitbucketLink[]>; } /** * Represents a Bitbucket project branching model */ interface BitbucketProjectBranchingModel { type: "project_branching_model"; development: { name: string; use_mainbranch: boolean; }; production?: { name: string; use_mainbranch: boolean; }; branch_types: Array<{ kind: string; prefix: string; }>; links: Record<string, BitbucketLink[]>; } interface BitbucketConfig { baseUrl: string; token?: string; username?: string; password?: string; defaultWorkspace?: string; allowDangerousCommands?: boolean; } // Normalize Bitbucket configuration for backward compatibility and DX function normalizeBitbucketConfig(rawConfig: BitbucketConfig): BitbucketConfig { let normalizedConfig = { ...rawConfig }; try { const parsed = new URL(rawConfig.baseUrl); const host = parsed.hostname.toLowerCase(); // If users provide a web URL like https://bitbucket.org/<workspace>, // extract the workspace and switch to the public API base URL if (host === "bitbucket.org" || host === "www.bitbucket.org") { const segments = parsed.pathname.split("/").filter(Boolean); if (!normalizedConfig.defaultWorkspace && segments.length >= 1) { normalizedConfig.defaultWorkspace = segments[0]; } normalizedConfig.baseUrl = "https://api.bitbucket.org/2.0"; } // If users provide https://api.bitbucket.org (without /2.0), ensure /2.0 if (host === "api.bitbucket.org") { const pathname = parsed.pathname.replace(/\/+$/, ""); if (!pathname.startsWith("/2.0")) { normalizedConfig.baseUrl = "https://api.bitbucket.org/2.0"; } else { normalizedConfig.baseUrl = "https://api.bitbucket.org/2.0"; } } // Remove trailing slashes for a consistent axios baseURL normalizedConfig.baseUrl = normalizedConfig.baseUrl.replace(/\/+$/, ""); } catch { // If baseUrl is not a valid absolute URL, keep as-is (custom/self-hosted cases) } return normalizedConfig; } /** * Represents a Bitbucket pipeline */ interface BitbucketPipeline { uuid: string; type: "pipeline"; build_number: number; creator: BitbucketAccount; repository: BitbucketRepository; target: BitbucketPipelineTarget; trigger: BitbucketPipelineTrigger; state: BitbucketPipelineState; created_on: string; completed_on?: string; build_seconds_used?: number; variables?: BitbucketPipelineVariable[]; configuration_sources?: BitbucketPipelineConfigurationSource[]; links: Record<string, BitbucketLink[]>; } /** * Represents a pipeline target */ interface BitbucketPipelineTarget { type: string; ref_type?: string; ref_name?: string; commit?: { type: "commit"; hash: string; }; selector?: { type: string; pattern: string; }; } /** * Represents a pipeline trigger */ interface BitbucketPipelineTrigger { type: string; name?: string; } /** * Represents a pipeline state */ interface BitbucketPipelineState { type: string; name: | "PENDING" | "IN_PROGRESS" | "SUCCESSFUL" | "FAILED" | "ERROR" | "STOPPED"; result?: { type: string; name: "SUCCESSFUL" | "FAILED" | "ERROR" | "STOPPED"; }; } /** * Represents a pipeline variable */ interface BitbucketPipelineVariable { type: "pipeline_variable"; key: string; value: string; secured?: boolean; } /** * Represents a pipeline configuration source */ interface BitbucketPipelineConfigurationSource { source: string; uri: string; } /** * Represents a pipeline step */ interface BitbucketPipelineStep { uuid: string; type: "pipeline_step"; name?: string; started_on?: string; completed_on?: string; state: BitbucketPipelineState; image?: { name: string; username?: string; password?: string; email?: string; }; setup_commands?: BitbucketPipelineCommand[]; script_commands?: BitbucketPipelineCommand[]; } /** * Represents a pipeline command */ interface BitbucketPipelineCommand { name?: string; command: string; } // =========== MCP SERVER =========== class BitbucketServer { private readonly server: Server; private readonly api: AxiosInstance; private readonly config: BitbucketConfig; private readonly dangerousToolNames = new Set<string>([ "deletePullRequestComment", "deletePullRequestTask", ]); private isDangerousTool(name: string): boolean { // Explicitly dangerous or conservative prefix match (delete*) if (this.dangerousToolNames.has(name)) return true; if (/^delete/i.test(name)) return true; return false; } constructor() { // Initialize with the older Server class pattern this.server = new Server( { name: "bitbucket-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Configuration from environment variables const initialConfig: BitbucketConfig = { baseUrl: process.env.BITBUCKET_URL ?? "https://api.bitbucket.org/2.0", token: process.env.BITBUCKET_TOKEN, username: process.env.BITBUCKET_USERNAME, password: process.env.BITBUCKET_PASSWORD, defaultWorkspace: process.env.BITBUCKET_WORKSPACE, }; const normalizedConfig = normalizeBitbucketConfig(initialConfig); if ( normalizedConfig.baseUrl !== initialConfig.baseUrl || normalizedConfig.defaultWorkspace !== initialConfig.defaultWorkspace ) { logger.info("Normalized Bitbucket configuration", { fromBaseUrl: initialConfig.baseUrl, toBaseUrl: normalizedConfig.baseUrl, defaultWorkspace: normalizedConfig.defaultWorkspace, }); } // Parse dangerous commands toggle (off by default) const enableDangerousEnv = ( process.env.BITBUCKET_ENABLE_DANGEROUS ?? process.env.BITBUCKET_ALLOW_DANGEROUS ?? "" ) .toString() .toLowerCase(); const allowDangerousCommands = ["1", "true", "yes", "on"].includes( enableDangerousEnv ); this.config = { ...normalizedConfig, allowDangerousCommands }; // Validate required config if (!this.config.baseUrl) { throw new Error("BITBUCKET_URL is required"); } if (!this.config.token && !(this.config.username && this.config.password)) { throw new Error( "Either BITBUCKET_TOKEN or BITBUCKET_USERNAME/PASSWORD is required" ); } // Setup Axios instance this.api = axios.create({ baseURL: this.config.baseUrl, headers: this.config.token ? { Authorization: `Bearer ${this.config.token}` } : { "Content-Type": "application/json" }, auth: this.config.username && this.config.password ? { username: this.config.username, password: this.config.password } : undefined, }); // Setup tool handlers using the request handler pattern this.setupToolHandlers(); // Add error handler - CRITICAL for stability this.server.onerror = (error) => logger.error("[MCP Error]", error); } private setupToolHandlers() { // Register the list tools handler this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "listRepositories", description: "List Bitbucket repositories", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, limit: { type: "number", description: "Maximum number of repositories to return", }, name: { type: "string", description: "Filter repositories by name (partial match supported)", }, }, }, }, { name: "getRepository", description: "Get repository details", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, }, required: ["workspace", "repo_slug"], }, }, { name: "getPullRequests", description: "Get pull requests for a repository", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, state: { type: "string", enum: ["OPEN", "MERGED", "DECLINED", "SUPERSEDED"], description: "Pull request state", }, limit: { type: "number", description: "Maximum number of pull requests to return", }, }, required: ["workspace", "repo_slug"], }, }, { name: "createPullRequest", description: "Create a new pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, title: { type: "string", description: "Pull request title" }, description: { type: "string", description: "Pull request description", }, sourceBranch: { type: "string", description: "Source branch name", }, targetBranch: { type: "string", description: "Target branch name", }, reviewers: { type: "array", items: { type: "string" }, description: "List of reviewer usernames", }, draft: { type: "boolean", description: "Whether to create the pull request as a draft", }, }, required: [ "workspace", "repo_slug", "title", "description", "sourceBranch", "targetBranch", ], }, }, { name: "getPullRequest", description: "Get details for a specific pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "updatePullRequest", description: "Update a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, title: { type: "string", description: "New pull request title" }, description: { type: "string", description: "New pull request description", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "getPullRequestActivity", description: "Get activity log for a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "approvePullRequest", description: "Approve a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "unapprovePullRequest", description: "Remove approval from a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "declinePullRequest", description: "Decline a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, message: { type: "string", description: "Reason for declining" }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "mergePullRequest", description: "Merge a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, message: { type: "string", description: "Merge commit message" }, strategy: { type: "string", enum: ["merge-commit", "squash", "fast-forward"], description: "Merge strategy", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "getPullRequestComments", description: "List comments on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "getPullRequestDiff", description: "Get diff for a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "getPullRequestCommits", description: "Get commits on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "addPullRequestComment", description: "Add a comment to a pull request (general or inline)", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, content: { type: "string", description: "Comment content in markdown format", }, pending: { type: "boolean", description: "Whether to create this comment as a pending comment (draft state)", }, inline: { type: "object", description: "Inline comment information for commenting on specific lines", properties: { path: { type: "string", description: "Path to the file in the repository", }, from: { type: "number", description: "Line number in the old version of the file (for deleted or modified lines)", }, to: { type: "number", description: "Line number in the new version of the file (for added or modified lines)", }, }, required: ["path"], }, }, required: ["workspace", "repo_slug", "pull_request_id", "content"], }, }, { name: "addPendingPullRequestComment", description: "Add a pending (draft) comment to a pull request that can be published later", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, content: { type: "string", description: "Comment content in markdown format", }, inline: { type: "object", description: "Inline comment information for commenting on specific lines", properties: { path: { type: "string", description: "Path to the file in the repository", }, from: { type: "number", description: "Line number in the old version of the file (for deleted or modified lines)", }, to: { type: "number", description: "Line number in the new version of the file (for added or modified lines)", }, }, required: ["path"], }, }, required: ["workspace", "repo_slug", "pull_request_id", "content"], }, }, { name: "publishPendingComments", description: "Publish all pending comments for a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "getRepositoryBranchingModel", description: "Get the branching model for a repository", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, }, required: ["workspace", "repo_slug"], }, }, { name: "getRepositoryBranchingModelSettings", description: "Get the branching model config for a repository", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, }, required: ["workspace", "repo_slug"], }, }, { name: "updateRepositoryBranchingModelSettings", description: "Update the branching model config for a repository", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, development: { type: "object", description: "Development branch settings", properties: { name: { type: "string", description: "Branch name" }, use_mainbranch: { type: "boolean", description: "Use main branch", }, }, }, production: { type: "object", description: "Production branch settings", properties: { name: { type: "string", description: "Branch name" }, use_mainbranch: { type: "boolean", description: "Use main branch", }, enabled: { type: "boolean", description: "Enable production branch", }, }, }, branch_types: { type: "array", description: "Branch types configuration", items: { type: "object", properties: { kind: { type: "string", description: "Branch type kind (e.g., bugfix, feature)", }, prefix: { type: "string", description: "Branch prefix" }, enabled: { type: "boolean", description: "Enable this branch type", }, }, required: ["kind"], }, }, }, required: ["workspace", "repo_slug"], }, }, { name: "getEffectiveRepositoryBranchingModel", description: "Get the effective branching model for a repository", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, }, required: ["workspace", "repo_slug"], }, }, { name: "getProjectBranchingModel", description: "Get the branching model for a project", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, project_key: { type: "string", description: "Project key" }, }, required: ["workspace", "project_key"], }, }, { name: "getProjectBranchingModelSettings", description: "Get the branching model config for a project", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, project_key: { type: "string", description: "Project key" }, }, required: ["workspace", "project_key"], }, }, { name: "updateProjectBranchingModelSettings", description: "Update the branching model config for a project", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, project_key: { type: "string", description: "Project key" }, development: { type: "object", description: "Development branch settings", properties: { name: { type: "string", description: "Branch name" }, use_mainbranch: { type: "boolean", description: "Use main branch", }, }, }, production: { type: "object", description: "Production branch settings", properties: { name: { type: "string", description: "Branch name" }, use_mainbranch: { type: "boolean", description: "Use main branch", }, enabled: { type: "boolean", description: "Enable production branch", }, }, }, branch_types: { type: "array", description: "Branch types configuration", items: { type: "object", properties: { kind: { type: "string", description: "Branch type kind (e.g., bugfix, feature)", }, prefix: { type: "string", description: "Branch prefix" }, enabled: { type: "boolean", description: "Enable this branch type", }, }, required: ["kind"], }, }, }, required: ["workspace", "project_key"], }, }, { name: "createDraftPullRequest", description: "Create a new draft pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, title: { type: "string", description: "Pull request title" }, description: { type: "string", description: "Pull request description", }, sourceBranch: { type: "string", description: "Source branch name", }, targetBranch: { type: "string", description: "Target branch name", }, reviewers: { type: "array", items: { type: "string" }, description: "List of reviewer usernames", }, }, required: [ "workspace", "repo_slug", "title", "description", "sourceBranch", "targetBranch", ], }, }, { name: "publishDraftPullRequest", description: "Publish a draft pull request to make it ready for review", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "convertTodraft", description: "Convert a regular pull request to draft status", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "getPendingReviewPRs", description: "List all open pull requests in the workspace where the authenticated user is a reviewer and has not yet approved.", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name (optional, defaults to BITBUCKET_WORKSPACE)", }, limit: { type: "number", description: "Maximum number of PRs to return (optional)", }, repositoryList: { type: "array", items: { type: "string" }, description: "List of repository slugs to check (optional)", }, }, }, }, { name: "listPipelineRuns", description: "List pipeline runs for a repository", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, limit: { type: "number", description: "Maximum number of pipelines to return", }, status: { type: "string", enum: [ "PENDING", "IN_PROGRESS", "SUCCESSFUL", "FAILED", "ERROR", "STOPPED", ], description: "Filter pipelines by status", }, target_branch: { type: "string", description: "Filter pipelines by target branch", }, trigger_type: { type: "string", enum: ["manual", "push", "pullrequest", "schedule"], description: "Filter pipelines by trigger type", }, }, required: ["workspace", "repo_slug"], }, }, { name: "getPipelineRun", description: "Get details for a specific pipeline run", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pipeline_uuid: { type: "string", description: "Pipeline UUID", }, }, required: ["workspace", "repo_slug", "pipeline_uuid"], }, }, { name: "runPipeline", description: "Trigger a new pipeline run", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, target: { type: "object", description: "Pipeline target configuration", properties: { ref_type: { type: "string", enum: ["branch", "tag", "bookmark", "named_branch"], description: "Reference type", }, ref_name: { type: "string", description: "Reference name (branch, tag, etc.)", }, commit_hash: { type: "string", description: "Specific commit hash to run pipeline on", }, selector_type: { type: "string", enum: ["default", "custom", "pull-requests"], description: "Pipeline selector type", }, selector_pattern: { type: "string", description: "Pipeline selector pattern (for custom pipelines)", }, }, required: ["ref_type", "ref_name"], }, variables: { type: "array", description: "Pipeline variables", items: { type: "object", properties: { key: { type: "string", description: "Variable name" }, value: { type: "string", description: "Variable value" }, secured: { type: "boolean", description: "Whether the variable is secured", }, }, required: ["key", "value"], }, }, }, required: ["workspace", "repo_slug", "target"], }, }, { name: "stopPipeline", description: "Stop a running pipeline", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pipeline_uuid: { type: "string", description: "Pipeline UUID", }, }, required: ["workspace", "repo_slug", "pipeline_uuid"], }, }, { name: "getPipelineSteps", description: "List steps for a pipeline run", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pipeline_uuid: { type: "string", description: "Pipeline UUID", }, }, required: ["workspace", "repo_slug", "pipeline_uuid"], }, }, { name: "getPipelineStep", description: "Get details for a specific pipeline step", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pipeline_uuid: { type: "string", description: "Pipeline UUID", }, step_uuid: { type: "string", description: "Step UUID", }, }, required: ["workspace", "repo_slug", "pipeline_uuid", "step_uuid"], }, }, { name: "getPipelineStepLogs", description: "Get logs for a specific pipeline step", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pipeline_uuid: { type: "string", description: "Pipeline UUID", }, step_uuid: { type: "string", description: "Step UUID", }, }, required: ["workspace", "repo_slug", "pipeline_uuid", "step_uuid"], }, }, { name: "getPullRequestComment", description: "Get a specific comment on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, comment_id: { type: "string", description: "Comment ID" }, }, required: [ "workspace", "repo_slug", "pull_request_id", "comment_id", ], }, }, { name: "updatePullRequestComment", description: "Update a comment on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, comment_id: { type: "string", description: "Comment ID" }, content: { type: "string", description: "Updated comment content", }, }, required: [ "workspace", "repo_slug", "pull_request_id", "comment_id", "content", ], }, }, { name: "deletePullRequestComment", description: "Delete a comment on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, comment_id: { type: "string", description: "Comment ID" }, }, required: [ "workspace", "repo_slug", "pull_request_id", "comment_id", ], }, }, { name: "resolveComment", description: "Resolve a comment thread on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, comment_id: { type: "string", description: "Comment ID" }, }, required: [ "workspace", "repo_slug", "pull_request_id", "comment_id", ], }, }, { name: "reopenComment", description: "Reopen a resolved comment thread on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, comment_id: { type: "string", description: "Comment ID" }, }, required: [ "workspace", "repo_slug", "pull_request_id", "comment_id", ], }, }, { name: "getPullRequestDiffStat", description: "Get diff statistics for a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "getPullRequestPatch", description: "Get patch for a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "getPullRequestTasks", description: "List tasks on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, { name: "createPullRequestTask", description: "Create a task on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, content: { type: "string", description: "Task content" }, comment: { type: "number", description: "Optional comment ID to attach the task", }, state: { type: "string", enum: ["OPEN", "RESOLVED"], description: "Initial task state", }, }, required: ["workspace", "repo_slug", "pull_request_id", "content"], }, }, { name: "getPullRequestTask", description: "Get a specific task on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, task_id: { type: "string", description: "Task ID" }, }, required: ["workspace", "repo_slug", "pull_request_id", "task_id"], }, }, { name: "updatePullRequestTask", description: "Update a task on a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, task_id: { type: "string", description: "Task ID" }, content: { type: "string", description: "Updated task content" }, state: { type: "string", enum: ["OPEN", "RESOLVED"], description: "Updated task state", }, }, required: ["workspace", "repo_slug", "pull_request_id", "task_id"], }, }, { name: "deletePullRequestTask", description: "Delete a task from a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, task_id: { type: "string", description: "Task ID" }, }, required: ["workspace", "repo_slug", "pull_request_id", "task_id"], }, }, { name: "getPullRequestStatuses", description: "List commit statuses associated with a pull request", inputSchema: { type: "object", properties: { workspace: { type: "string", description: "Bitbucket workspace name", }, repo_slug: { type: "string", description: "Repository slug" }, pull_request_id: { type: "string", description: "Pull request ID", }, }, required: ["workspace", "repo_slug", "pull_request_id"], }, }, ].filter( (tool) => this.config.allowDangerousCommands === true || !this.isDangerousTool(tool.name) ), })); // Register the call tool handler this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { logger.info(`Called tool: ${request.params.name}`, { arguments: request.params.arguments, }); const args = request.params.arguments ?? {}; const toolName = request.params.name; // Guard dangerous tools when not enabled if ( this.isDangerousTool(toolName) && this.config.allowDangerousCommands !== true ) { throw new McpError( ErrorCode.MethodNotFound, `Tool ${toolName} is disabled. Set BITBUCKET_ENABLE_DANGEROUS=true to enable.` ); } switch (request.params.name) { case "listRepositories": return await this.listRepositories( args.workspace as string, args.limit as number, args.name as string ); case "getRepository": return await this.getRepository( args.workspace as string, args.repo_slug as string ); case "getPullRequests": return await this.getPullRequests( args.workspace as string, args.repo_slug as string, args.state as "OPEN" | "MERGED" | "DECLINED" | "SUPERSEDED", args.limit as number ); case "createPullRequest": return await this.createPullRequest( args.workspace as string, args.repo_slug as string, args.title as string, args.description as string, args.sourceBranch as string, args.targetBranch as string, args.reviewers as string[], args.draft as boolean ); case "getPullRequest": return await this.getPullRequest( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "updatePullRequest": return await this.updatePullRequest( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.title as string, args.description as string ); case "getPullRequestActivity": return await this.getPullRequestActivity( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "approvePullRequest": return await this.approvePullRequest( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "unapprovePullRequest": return await this.unapprovePullRequest( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "declinePullRequest": return await this.declinePullRequest( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.message as string ); case "mergePullRequest": return await this.mergePullRequest( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.message as string, args.strategy as "merge-commit" | "squash" | "fast-forward" ); case "getPullRequestComments": return await this.getPullRequestComments( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "getPullRequestDiff": return await this.getPullRequestDiff( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "getPullRequestCommits": return await this.getPullRequestCommits( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "addPullRequestComment": return await this.addPullRequestComment( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.content as string, args.inline as InlineCommentInline, args.pending as boolean ); case "addPendingPullRequestComment": return await this.addPendingPullRequestComment( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.content as string, args.inline as InlineCommentInline ); case "publishPendingComments": return await this.publishPendingComments( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "getRepositoryBranchingModel": return await this.getRepositoryBranchingModel( args.workspace as string, args.repo_slug as string ); case "getRepositoryBranchingModelSettings": return await this.getRepositoryBranchingModelSettings( args.workspace as string, args.repo_slug as string ); case "updateRepositoryBranchingModelSettings": return await this.updateRepositoryBranchingModelSettings( args.workspace as string, args.repo_slug as string, args.development as Record<string, any>, args.production as Record<string, any>, args.branch_types as Array<Record<string, any>> ); case "getEffectiveRepositoryBranchingModel": return await this.getEffectiveRepositoryBranchingModel( args.workspace as string, args.repo_slug as string ); case "getProjectBranchingModel": return await this.getProjectBranchingModel( args.workspace as string, args.project_key as string ); case "getProjectBranchingModelSettings": return await this.getProjectBranchingModelSettings( args.workspace as string, args.project_key as string ); case "updateProjectBranchingModelSettings": return await this.updateProjectBranchingModelSettings( args.workspace as string, args.project_key as string, args.development as Record<string, any>, args.production as Record<string, any>, args.branch_types as Array<Record<string, any>> ); case "createDraftPullRequest": return await this.createDraftPullRequest( args.workspace as string, args.repo_slug as string, args.title as string, args.description as string, args.sourceBranch as string, args.targetBranch as string, args.reviewers as string[] ); case "publishDraftPullRequest": return await this.publishDraftPullRequest( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "convertTodraft": return await this.convertTodraft( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "getPendingReviewPRs": return await this.getPendingReviewPRs( args.workspace as string | undefined, args.limit as number, args.repositoryList as string[] ); case "listPipelineRuns": return await this.listPipelineRuns( args.workspace as string, args.repo_slug as string, args.limit as number, args.status as | "PENDING" | "IN_PROGRESS" | "SUCCESSFUL" | "FAILED" | "ERROR" | "STOPPED", args.target_branch as string, args.trigger_type as | "manual" | "push" | "pullrequest" | "schedule" ); case "getPipelineRun": return await this.getPipelineRun( args.workspace as string, args.repo_slug as string, args.pipeline_uuid as string ); case "runPipeline": return await this.runPipeline( args.workspace as string, args.repo_slug as string, args.target as any, args.variables as any[] ); case "stopPipeline": return await this.stopPipeline( args.workspace as string, args.repo_slug as string, args.pipeline_uuid as string ); case "getPipelineSteps": return await this.getPipelineSteps( args.workspace as string, args.repo_slug as string, args.pipeline_uuid as string ); case "getPipelineStep": return await this.getPipelineStep( args.workspace as string, args.repo_slug as string, args.pipeline_uuid as string, args.step_uuid as string ); case "getPipelineStepLogs": return await this.getPipelineStepLogs( args.workspace as string, args.repo_slug as string, args.pipeline_uuid as string, args.step_uuid as string ); case "getPullRequestComment": return await this.getPullRequestComment( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.comment_id as string ); case "updatePullRequestComment": return await this.updatePullRequestComment( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.comment_id as string, args.content as string ); case "deletePullRequestComment": return await this.deletePullRequestComment( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.comment_id as string ); case "resolveComment": return await this.setCommentResolved( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.comment_id as string, true ); case "reopenComment": return await this.setCommentResolved( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.comment_id as string, false ); case "getPullRequestDiffStat": return await this.getPullRequestDiffStat( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "getPullRequestPatch": return await this.getPullRequestPatch( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "getPullRequestTasks": return await this.getPullRequestTasks( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); case "createPullRequestTask": return await this.createPullRequestTask( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.content as string, args.comment as number, args.state as "OPEN" | "RESOLVED" ); case "getPullRequestTask": return await this.getPullRequestTask( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.task_id as string ); case "updatePullRequestTask": return await this.updatePullRequestTask( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.task_id as string, args.content as string | undefined, args.state as ("OPEN" | "RESOLVED") | undefined ); case "deletePullRequestTask": return await this.deletePullRequestTask( args.workspace as string, args.repo_slug as string, args.pull_request_id as string, args.task_id as string ); case "getPullRequestStatuses": return await this.getPullRequestStatuses( args.workspace as string, args.repo_slug as string, args.pull_request_id as string ); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } } catch (error) { logger.error("Tool execution error", { error }); if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `Bitbucket API error: ${ error.response?.data.message ?? error.message }` ); } throw error; } }); } async listRepositories( workspace?: string, limit: number = 10, name?: string ) { try { // Use default workspace if not provided const wsName = workspace || this.config.defaultWorkspace; if (!wsName) { throw new McpError( ErrorCode.InvalidParams, "Workspace must be provided either as a parameter or through BITBUCKET_WORKSPACE environment variable" ); } logger.info("Listing Bitbucket repositories", { workspace: wsName, limit, name, }); // Build query parameters const params: Record<string, any> = { limit }; if (name) { params.q = `name~"${name}"`; } const response = await this.api.get(`/repositories/${wsName}`, { params, }); // Use the results from Bitbucket API directly let repositories = response.data.values; return { content: [ { type: "text", text: JSON.stringify(repositories, null, 2), }, ], }; } catch (error) { logger.error("Error listing repositories", { error, workspace, name }); throw new McpError( ErrorCode.InternalError, `Failed to list repositories: ${ error instanceof Error ? error.message : String(error) }` ); } } async getRepository(workspace: string, repo_slug: string) { try { logger.info("Getting Bitbucket repository info", { workspace, repo_slug, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting repository", { error, workspace, repo_slug }); throw new McpError( ErrorCode.InternalError, `Failed to get repository: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequests( workspace: string, repo_slug: string, state?: "OPEN" | "MERGED" | "DECLINED" | "SUPERSEDED", limit: number = 10 ) { try { logger.info("Getting Bitbucket pull requests", { workspace, repo_slug, state, limit, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests`, { params: { state: state, limit, }, } ); return { content: [ { type: "text", text: JSON.stringify(response.data.values, null, 2), }, ], }; } catch (error) { logger.error("Error getting pull requests", { error, workspace, repo_slug, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull requests: ${ error instanceof Error ? error.message : String(error) }` ); } } async createPullRequest( workspace: string, repo_slug: string, title: string, description: string, sourceBranch: string, targetBranch: string, reviewers?: string[], draft?: boolean ) { try { logger.info("Creating Bitbucket pull request", { workspace, repo_slug, title, sourceBranch, targetBranch, }); // Prepare reviewers format if provided const reviewersArray = reviewers?.map((username) => ({ username, })) || []; // Create the pull request const response = await this.api.post( `/repositories/${workspace}/${repo_slug}/pullrequests`, { title, description, source: { branch: { name: sourceBranch, }, }, destination: { branch: { name: targetBranch, }, }, reviewers: reviewersArray, close_source_branch: true, draft: draft === true, // Only set draft=true if explicitly specified } ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error creating pull request", { error, workspace, repo_slug, }); throw new McpError( ErrorCode.InternalError, `Failed to create pull request: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequest( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Getting Bitbucket pull request details", { workspace, repo_slug, pull_request_id, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting pull request details", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request details: ${ error instanceof Error ? error.message : String(error) }` ); } } async updatePullRequest( workspace: string, repo_slug: string, pull_request_id: string, title?: string, description?: string ) { try { logger.info("Updating Bitbucket pull request", { workspace, repo_slug, pull_request_id, }); // Only include fields that are provided const updateData: Record<string, any> = {}; if (title !== undefined) updateData.title = title; if (description !== undefined) updateData.description = description; const response = await this.api.put( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}`, updateData ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error updating pull request", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to update pull request: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestActivity( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Getting Bitbucket pull request activity", { workspace, repo_slug, pull_request_id, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/activity` ); return { content: [ { type: "text", text: JSON.stringify(response.data.values, null, 2), }, ], }; } catch (error) { logger.error("Error getting pull request activity", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request activity: ${ error instanceof Error ? error.message : String(error) }` ); } } async approvePullRequest( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Approving Bitbucket pull request", { workspace, repo_slug, pull_request_id, }); const response = await this.api.post( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/approve` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error approving pull request", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to approve pull request: ${ error instanceof Error ? error.message : String(error) }` ); } } async unapprovePullRequest( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Unapproving Bitbucket pull request", { workspace, repo_slug, pull_request_id, }); const response = await this.api.delete( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/approve` ); return { content: [ { type: "text", text: "Pull request approval removed successfully.", }, ], }; } catch (error) { logger.error("Error unapproving pull request", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to unapprove pull request: ${ error instanceof Error ? error.message : String(error) }` ); } } async declinePullRequest( workspace: string, repo_slug: string, pull_request_id: string, message?: string ) { try { logger.info("Declining Bitbucket pull request", { workspace, repo_slug, pull_request_id, }); // Include message if provided const data = message ? { message } : {}; const response = await this.api.post( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/decline`, data ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error declining pull request", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to decline pull request: ${ error instanceof Error ? error.message : String(error) }` ); } } async mergePullRequest( workspace: string, repo_slug: string, pull_request_id: string, message?: string, strategy?: "merge-commit" | "squash" | "fast-forward" ) { try { logger.info("Merging Bitbucket pull request", { workspace, repo_slug, pull_request_id, strategy, }); // Build request data const data: Record<string, any> = {}; if (message) data.message = message; if (strategy) data.merge_strategy = strategy; const response = await this.api.post( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/merge`, data ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error merging pull request", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to merge pull request: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestComments( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Getting Bitbucket pull request comments", { workspace, repo_slug, pull_request_id, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/comments` ); return { content: [ { type: "text", text: JSON.stringify(response.data.values, null, 2), }, ], }; } catch (error) { logger.error("Error getting pull request comments", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request comments: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestDiff( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Getting Bitbucket pull request diff", { workspace, repo_slug, pull_request_id, }); // First get the pull request details to extract commit information const prResponse = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}` ); const sourceCommit = prResponse.data.source.commit.hash; const destinationCommit = prResponse.data.destination.commit.hash; // Construct the correct diff URL with the proper format // The format is: /repositories/{workspace}/{repo_slug}/diff/{source_repo}:{source_commit}%0D{destination_commit}?from_pullrequest_id={pr_id}&topic=true const diffUrl = `/repositories/${workspace}/${repo_slug}/diff/${workspace}/${repo_slug}:${sourceCommit}%0D${destinationCommit}?from_pullrequest_id=${pull_request_id}&topic=true`; const response = await this.api.get(diffUrl, { headers: { Accept: "text/plain", }, responseType: "text", maxRedirects: 5, // Enable redirect following }); return { content: [ { type: "text", text: response.data, }, ], }; } catch (error) { logger.error("Error getting pull request diff", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request diff: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestCommits( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Getting Bitbucket pull request commits", { workspace, repo_slug, pull_request_id, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/commits` ); return { content: [ { type: "text", text: JSON.stringify(response.data.values, null, 2), }, ], }; } catch (error) { logger.error("Error getting pull request commits", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request commits: ${ error instanceof Error ? error.message : String(error) }` ); } } async addPullRequestComment( workspace: string, repo_slug: string, pull_request_id: string, content: string, inline?: InlineCommentInline, pending?: boolean ) { try { logger.info("Adding comment to Bitbucket pull request", { workspace, repo_slug, pull_request_id, inline: inline ? "inline comment" : "general comment", }); // Prepare the comment data const commentData: any = { content: { raw: content, }, }; // Add pending flag if provided if (pending !== undefined) { commentData.pending = pending; } // Add inline information if provided if (inline) { commentData.inline = { path: inline.path, }; // Add line number information based on the type if (inline.from !== undefined) { commentData.inline.from = inline.from; } if (inline.to !== undefined) { commentData.inline.to = inline.to; } } const response = await this.api.post( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/comments`, commentData ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error adding comment to pull request", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to add pull request comment: ${ error instanceof Error ? error.message : String(error) }` ); } } async getRepositoryBranchingModel(workspace: string, repo_slug: string) { try { logger.info("Getting repository branching model", { workspace, repo_slug, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/branching-model` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting repository branching model", { error, workspace, repo_slug, }); throw new McpError( ErrorCode.InternalError, `Failed to get repository branching model: ${ error instanceof Error ? error.message : String(error) }` ); } } async getRepositoryBranchingModelSettings( workspace: string, repo_slug: string ) { try { logger.info("Getting repository branching model settings", { workspace, repo_slug, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/branching-model/settings` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting repository branching model settings", { error, workspace, repo_slug, }); throw new McpError( ErrorCode.InternalError, `Failed to get repository branching model settings: ${ error instanceof Error ? error.message : String(error) }` ); } } async updateRepositoryBranchingModelSettings( workspace: string, repo_slug: string, development?: Record<string, any>, production?: Record<string, any>, branch_types?: Array<Record<string, any>> ) { try { logger.info("Updating repository branching model settings", { workspace, repo_slug, development, production, branch_types, }); // Build request data with only the fields that are provided const updateData: Record<string, any> = {}; if (development) updateData.development = development; if (production) updateData.production = production; if (branch_types) updateData.branch_types = branch_types; const response = await this.api.put( `/repositories/${workspace}/${repo_slug}/branching-model/settings`, updateData ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error updating repository branching model settings", { error, workspace, repo_slug, }); throw new McpError( ErrorCode.InternalError, `Failed to update repository branching model settings: ${ error instanceof Error ? error.message : String(error) }` ); } } async getEffectiveRepositoryBranchingModel( workspace: string, repo_slug: string ) { try { logger.info("Getting effective repository branching model", { workspace, repo_slug, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/effective-branching-model` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting effective repository branching model", { error, workspace, repo_slug, }); throw new McpError( ErrorCode.InternalError, `Failed to get effective repository branching model: ${ error instanceof Error ? error.message : String(error) }` ); } } async getProjectBranchingModel(workspace: string, project_key: string) { try { logger.info("Getting project branching model", { workspace, project_key, }); const response = await this.api.get( `/workspaces/${workspace}/projects/${project_key}/branching-model` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting project branching model", { error, workspace, project_key, }); throw new McpError( ErrorCode.InternalError, `Failed to get project branching model: ${ error instanceof Error ? error.message : String(error) }` ); } } async getProjectBranchingModelSettings( workspace: string, project_key: string ) { try { logger.info("Getting project branching model settings", { workspace, project_key, }); const response = await this.api.get( `/workspaces/${workspace}/projects/${project_key}/branching-model/settings` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting project branching model settings", { error, workspace, project_key, }); throw new McpError( ErrorCode.InternalError, `Failed to get project branching model settings: ${ error instanceof Error ? error.message : String(error) }` ); } } async updateProjectBranchingModelSettings( workspace: string, project_key: string, development?: Record<string, any>, production?: Record<string, any>, branch_types?: Array<Record<string, any>> ) { try { logger.info("Updating project branching model settings", { workspace, project_key, development, production, branch_types, }); // Build request data with only the fields that are provided const updateData: Record<string, any> = {}; if (development) updateData.development = development; if (production) updateData.production = production; if (branch_types) updateData.branch_types = branch_types; const response = await this.api.put( `/workspaces/${workspace}/projects/${project_key}/branching-model/settings`, updateData ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error updating project branching model settings", { error, workspace, project_key, }); throw new McpError( ErrorCode.InternalError, `Failed to update project branching model settings: ${ error instanceof Error ? error.message : String(error) }` ); } } async addPendingPullRequestComment( workspace: string, repo_slug: string, pull_request_id: string, content: string, inline?: InlineCommentInline ) { try { logger.info("Adding pending comment to Bitbucket pull request", { workspace, repo_slug, pull_request_id, inline: inline ? "inline comment" : "general comment", }); // Use the existing addPullRequestComment method with pending=true return await this.addPullRequestComment( workspace, repo_slug, pull_request_id, content, inline, true // Set pending to true for draft comment ); } catch (error) { logger.error("Error adding pending comment to pull request", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to add pending pull request comment: ${ error instanceof Error ? error.message : String(error) }` ); } } async publishPendingComments( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Publishing pending comments for Bitbucket pull request", { workspace, repo_slug, pull_request_id, }); // First, get all pending comments for the pull request const commentsResponse = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/comments` ); const comments = commentsResponse.data.values || []; const pendingComments = comments.filter( (comment: any) => comment.pending === true ); if (pendingComments.length === 0) { return { content: [ { type: "text", text: "No pending comments found to publish.", }, ], }; } // Publish each pending comment by updating it with pending=false const publishResults = []; for (const comment of pendingComments) { try { const updateResponse = await this.api.put( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/comments/${comment.id}`, { content: comment.content, pending: false, ...(comment.inline && { inline: comment.inline }), } ); publishResults.push({ commentId: comment.id, status: "published", data: updateResponse.data, }); } catch (error) { publishResults.push({ commentId: comment.id, status: "error", error: error instanceof Error ? error.message : String(error), }); } } return { content: [ { type: "text", text: JSON.stringify( { message: `Published ${pendingComments.length} pending comments`, results: publishResults, }, null, 2 ), }, ], }; } catch (error) { logger.error("Error publishing pending comments", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to publish pending comments: ${ error instanceof Error ? error.message : String(error) }` ); } } async createDraftPullRequest( workspace: string, repo_slug: string, title: string, description: string, sourceBranch: string, targetBranch: string, reviewers?: string[] ) { try { logger.info("Creating draft Bitbucket pull request", { workspace, repo_slug, title, sourceBranch, targetBranch, }); // Use the existing createPullRequest method with draft=true return await this.createPullRequest( workspace, repo_slug, title, description, sourceBranch, targetBranch, reviewers, true // Set draft to true ); } catch (error) { logger.error("Error creating draft pull request", { error, workspace, repo_slug, }); throw new McpError( ErrorCode.InternalError, `Failed to create draft pull request: ${ error instanceof Error ? error.message : String(error) }` ); } } async publishDraftPullRequest( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Publishing draft pull request", { workspace, repo_slug, pull_request_id, }); // Update the pull request to set draft=false const response = await this.api.put( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}`, { draft: false, } ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error publishing draft pull request", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to publish draft pull request: ${ error instanceof Error ? error.message : String(error) }` ); } } async convertTodraft( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Converting pull request to draft", { workspace, repo_slug, pull_request_id, }); // Update the pull request to set draft=true const response = await this.api.put( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}`, { draft: true, } ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error converting pull request to draft", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to convert pull request to draft: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPendingReviewPRs( workspace?: string, limit: number = 50, repositoryList?: string[] ) { try { const wsName = workspace || this.config.defaultWorkspace; if (!wsName) { throw new McpError( ErrorCode.InvalidParams, "Workspace must be provided either as a parameter or through BITBUCKET_WORKSPACE environment variable" ); } const currentUserNickname = this.config.username; if (!currentUserNickname) { throw new McpError( ErrorCode.InvalidParams, "Username must be provided through BITBUCKET_USERNAME environment variable" ); } logger.info("Getting pending review PRs", { workspace: wsName, username: currentUserNickname, repositoryList: repositoryList?.length || "all repositories", limit, }); let repositoriesToCheck: string[] = []; if (repositoryList && repositoryList.length > 0) { // Use the provided repository list repositoriesToCheck = repositoryList; logger.info( `Checking specific repositories: ${repositoryList.join(", ")}` ); } else { // Get all repositories in the workspace (existing behavior) logger.info("Getting all repositories in workspace..."); const reposResponse = await this.api.get(`/repositories/${wsName}`, { params: { pagelen: 100 }, }); if (!reposResponse.data.values) { throw new McpError( ErrorCode.InternalError, "Failed to fetch repositories" ); } repositoriesToCheck = reposResponse.data.values.map( (repo: any) => repo.name ); logger.info( `Found ${repositoriesToCheck.length} repositories to check` ); } const pendingPRs: any[] = []; const batchSize = 5; // Process repositories in batches to avoid overwhelming the API // Process repositories in batches for (let i = 0; i < repositoriesToCheck.length; i += batchSize) { const batch = repositoriesToCheck.slice(i, i + batchSize); // Process batch in parallel const batchPromises = batch.map(async (repoSlug) => { try { logger.info(`Checking repository: ${repoSlug}`); // Get open PRs for this repository with participants expanded const prsResponse = await this.api.get( `/repositories/${wsName}/${repoSlug}/pullrequests`, { params: { state: "OPEN", pagelen: Math.min(limit, 50), // Limit per repo to avoid too much data fields: "values.id,values.title,values.description,values.state,values.created_on,values.updated_on,values.author,values.source,values.destination,values.participants.user.nickname,values.participants.role,values.participants.approved,values.links", }, } ); if (!prsResponse.data.values) { return []; } // Filter PRs where current user is a reviewer and hasn't approved const reposPendingPRs = prsResponse.data.values.filter( (pr: any) => { if (!pr.participants || !Array.isArray(pr.participants)) { logger.debug(`PR ${pr.id} has no participants array`); return false; } logger.debug( `PR ${pr.id} participants:`, pr.participants.map((p: any) => ({ nickname: p.user?.nickname, role: p.role, approved: p.approved, })) ); // Check if current user is a reviewer who hasn't approved const userParticipant = pr.participants.find( (participant: any) => participant.user?.nickname === currentUserNickname && participant.role === "REVIEWER" && participant.approved === false ); logger.debug( `PR ${pr.id} - User ${currentUserNickname} is pending reviewer:`, !!userParticipant ); return !!userParticipant; } ); // Add repository info to each PR return reposPendingPRs.map((pr: any) => ({ ...pr, repository: { name: repoSlug, full_name: `${wsName}/${repoSlug}`, }, })); } catch (error) { logger.error(`Error checking repository ${repoSlug}:`, error); return []; } }); // Wait for batch to complete const batchResults = await Promise.all(batchPromises); // Flatten and add to results for (const repoPRs of batchResults) { pendingPRs.push(...repoPRs); // Stop if we've reached the limit if (pendingPRs.length >= limit) { break; } } // Stop processing if we've reached the limit if (pendingPRs.length >= limit) { break; } } // Trim to exact limit and sort by updated date const finalResults = pendingPRs .slice(0, limit) .sort( (a, b) => new Date(b.updated_on).getTime() - new Date(a.updated_on).getTime() ); logger.info(`Found ${finalResults.length} pending review PRs`); return { content: [ { type: "text", text: JSON.stringify( { pending_review_prs: finalResults, total_found: finalResults.length, searched_repositories: repositoriesToCheck.length, user: currentUserNickname, workspace: wsName, }, null, 2 ), }, ], }; } catch (error) { logger.error("Error getting pending review PRs:", error); throw new McpError( ErrorCode.InternalError, `Failed to get pending review PRs: ${ error instanceof Error ? error.message : String(error) }` ); } } // =========== PIPELINE METHODS =========== async listPipelineRuns( workspace: string, repo_slug: string, limit?: number, status?: | "PENDING" | "IN_PROGRESS" | "SUCCESSFUL" | "FAILED" | "ERROR" | "STOPPED", target_branch?: string, trigger_type?: "manual" | "push" | "pullrequest" | "schedule" ) { try { logger.info("Listing pipeline runs", { workspace, repo_slug, limit, status, target_branch, trigger_type, }); const params: Record<string, any> = {}; if (limit) params.pagelen = limit; if (status) params.status = status; if (target_branch) params["target.branch"] = target_branch; if (trigger_type) params.trigger_type = trigger_type; const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pipelines`, { params } ); return { content: [ { type: "text", text: JSON.stringify(response.data.values, null, 2), }, ], }; } catch (error) { logger.error("Error listing pipeline runs", { error, workspace, repo_slug, }); throw new McpError( ErrorCode.InternalError, `Failed to list pipeline runs: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPipelineRun( workspace: string, repo_slug: string, pipeline_uuid: string ) { try { logger.info("Getting pipeline run details", { workspace, repo_slug, pipeline_uuid, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pipelines/${pipeline_uuid}` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting pipeline run", { error, workspace, repo_slug, pipeline_uuid, }); throw new McpError( ErrorCode.InternalError, `Failed to get pipeline run: ${ error instanceof Error ? error.message : String(error) }` ); } } async runPipeline( workspace: string, repo_slug: string, target: any, variables?: any[] ) { try { logger.info("Triggering pipeline run", { workspace, repo_slug, target, variables: variables?.length || 0, }); // Build the target object based on the input const pipelineTarget: Record<string, any> = { type: target.commit_hash ? "pipeline_commit_target" : "pipeline_ref_target", ref_type: target.ref_type, ref_name: target.ref_name, }; // Add commit if specified if (target.commit_hash) { pipelineTarget.commit = { type: "commit", hash: target.commit_hash, }; } // Add selector if specified if (target.selector_type && target.selector_pattern) { pipelineTarget.selector = { type: target.selector_type, pattern: target.selector_pattern, }; } // Build the request data const requestData: Record<string, any> = { target: pipelineTarget, }; // Add variables if provided if (variables && variables.length > 0) { requestData.variables = variables.map((variable: any) => ({ key: variable.key, value: variable.value, secured: variable.secured || false, })); } const response = await this.api.post( `/repositories/${workspace}/${repo_slug}/pipelines`, requestData ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error running pipeline", { error, workspace, repo_slug, }); throw new McpError( ErrorCode.InternalError, `Failed to run pipeline: ${ error instanceof Error ? error.message : String(error) }` ); } } async stopPipeline( workspace: string, repo_slug: string, pipeline_uuid: string ) { try { logger.info("Stopping pipeline", { workspace, repo_slug, pipeline_uuid, }); const response = await this.api.post( `/repositories/${workspace}/${repo_slug}/pipelines/${pipeline_uuid}/stop` ); return { content: [ { type: "text", text: "Pipeline stop signal sent successfully.", }, ], }; } catch (error) { logger.error("Error stopping pipeline", { error, workspace, repo_slug, pipeline_uuid, }); throw new McpError( ErrorCode.InternalError, `Failed to stop pipeline: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPipelineSteps( workspace: string, repo_slug: string, pipeline_uuid: string ) { try { logger.info("Getting pipeline steps", { workspace, repo_slug, pipeline_uuid, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pipelines/${pipeline_uuid}/steps` ); return { content: [ { type: "text", text: JSON.stringify(response.data.values, null, 2), }, ], }; } catch (error) { logger.error("Error getting pipeline steps", { error, workspace, repo_slug, pipeline_uuid, }); throw new McpError( ErrorCode.InternalError, `Failed to get pipeline steps: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPipelineStep( workspace: string, repo_slug: string, pipeline_uuid: string, step_uuid: string ) { try { logger.info("Getting pipeline step details", { workspace, repo_slug, pipeline_uuid, step_uuid, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pipelines/${pipeline_uuid}/steps/${step_uuid}` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting pipeline step", { error, workspace, repo_slug, pipeline_uuid, step_uuid, }); throw new McpError( ErrorCode.InternalError, `Failed to get pipeline step: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPipelineStepLogs( workspace: string, repo_slug: string, pipeline_uuid: string, step_uuid: string ) { try { logger.info("Getting pipeline step logs", { workspace, repo_slug, pipeline_uuid, step_uuid, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pipelines/${pipeline_uuid}/steps/${step_uuid}/log`, { maxRedirects: 5, // Follow redirects to S3 responseType: "text", } ); return { content: [ { type: "text", text: response.data, }, ], }; } catch (error) { logger.error("Error getting pipeline step logs", { error, workspace, repo_slug, pipeline_uuid, step_uuid, }); throw new McpError( ErrorCode.InternalError, `Failed to get pipeline step logs: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestComment( workspace: string, repo_slug: string, pull_request_id: string, comment_id: string ) { try { logger.info("Getting pull request comment", { workspace, repo_slug, pull_request_id, comment_id, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/comments/${comment_id}` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error getting pull request comment", { error, workspace, repo_slug, pull_request_id, comment_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request comment: ${ error instanceof Error ? error.message : String(error) }` ); } } async updatePullRequestComment( workspace: string, repo_slug: string, pull_request_id: string, comment_id: string, content: string ) { try { logger.info("Updating pull request comment", { workspace, repo_slug, pull_request_id, comment_id, }); const response = await this.api.put( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/comments/${comment_id}`, { content: { raw: content }, } ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2) }, ], }; } catch (error) { logger.error("Error updating pull request comment", { error, workspace, repo_slug, pull_request_id, comment_id, }); throw new McpError( ErrorCode.InternalError, `Failed to update pull request comment: ${ error instanceof Error ? error.message : String(error) }` ); } } async deletePullRequestComment( workspace: string, repo_slug: string, pull_request_id: string, comment_id: string ) { try { logger.info("Deleting pull request comment", { workspace, repo_slug, pull_request_id, comment_id, }); await this.api.delete( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/comments/${comment_id}` ); return { content: [{ type: "text", text: "Comment deleted successfully." }], }; } catch (error) { logger.error("Error deleting pull request comment", { error, workspace, repo_slug, pull_request_id, comment_id, }); throw new McpError( ErrorCode.InternalError, `Failed to delete pull request comment: ${ error instanceof Error ? error.message : String(error) }` ); } } async setCommentResolved( workspace: string, repo_slug: string, pull_request_id: string, comment_id: string, resolved: boolean ) { try { logger.info("Setting comment resolved state", { workspace, repo_slug, pull_request_id, comment_id, resolved, }); const response = await this.api.put( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/comments/${comment_id}`, { resolved, } ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { logger.error("Error setting comment resolved state", { error, workspace, repo_slug, pull_request_id, comment_id, resolved, }); throw new McpError( ErrorCode.InternalError, `Failed to update comment resolved state: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestDiffStat( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Getting pull request diffstat", { workspace, repo_slug, pull_request_id, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/diffstat` ); return { content: [ { type: "text", text: JSON.stringify(response.data.values, null, 2) }, ], }; } catch (error) { logger.error("Error getting pull request diffstat", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request diffstat: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestPatch( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Getting pull request patch", { workspace, repo_slug, pull_request_id, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/patch`, { headers: { Accept: "text/plain" }, responseType: "text", maxRedirects: 5, } ); return { content: [{ type: "text", text: response.data }] }; } catch (error) { logger.error("Error getting pull request patch", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request patch: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestTasks( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Getting pull request tasks", { workspace, repo_slug, pull_request_id, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/tasks` ); return { content: [ { type: "text", text: JSON.stringify(response.data.values, null, 2) }, ], }; } catch (error) { logger.error("Error getting pull request tasks", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request tasks: ${ error instanceof Error ? error.message : String(error) }` ); } } async createPullRequestTask( workspace: string, repo_slug: string, pull_request_id: string, content: string, commentId?: number, state?: "OPEN" | "RESOLVED" ) { try { logger.info("Creating pull request task", { workspace, repo_slug, pull_request_id, }); const data: Record<string, any> = { content }; if (commentId) data.comment = { id: commentId }; if (state) data.state = state; const response = await this.api.post( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/tasks`, data ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2) }, ], }; } catch (error) { logger.error("Error creating pull request task", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to create pull request task: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestTask( workspace: string, repo_slug: string, pull_request_id: string, task_id: string ) { try { logger.info("Getting pull request task", { workspace, repo_slug, pull_request_id, task_id, }); const response = await this.api.get(`/tasks/${task_id}`); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2) }, ], }; } catch (error) { logger.error("Error getting pull request task", { error, workspace, repo_slug, pull_request_id, task_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request task: ${ error instanceof Error ? error.message : String(error) }` ); } } async updatePullRequestTask( workspace: string, repo_slug: string, pull_request_id: string, task_id: string, content?: string, state?: "OPEN" | "RESOLVED" ) { try { logger.info("Updating pull request task", { workspace, repo_slug, pull_request_id, task_id, }); const data: Record<string, any> = {}; if (content !== undefined) data.content = content; if (state !== undefined) data.state = state; const response = await this.api.put(`/tasks/${task_id}`, data); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2) }, ], }; } catch (error) { logger.error("Error updating pull request task", { error, workspace, repo_slug, pull_request_id, task_id, }); throw new McpError( ErrorCode.InternalError, `Failed to update pull request task: ${ error instanceof Error ? error.message : String(error) }` ); } } async deletePullRequestTask( workspace: string, repo_slug: string, pull_request_id: string, task_id: string ) { try { logger.info("Deleting pull request task", { workspace, repo_slug, pull_request_id, task_id, }); await this.api.delete(`/tasks/${task_id}`); return { content: [{ type: "text", text: "Task deleted successfully." }], }; } catch (error) { logger.error("Error deleting pull request task", { error, workspace, repo_slug, pull_request_id, task_id, }); throw new McpError( ErrorCode.InternalError, `Failed to delete pull request task: ${ error instanceof Error ? error.message : String(error) }` ); } } async getPullRequestStatuses( workspace: string, repo_slug: string, pull_request_id: string ) { try { logger.info("Getting pull request statuses", { workspace, repo_slug, pull_request_id, }); const response = await this.api.get( `/repositories/${workspace}/${repo_slug}/pullrequests/${pull_request_id}/statuses` ); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2) }, ], }; } catch (error) { logger.error("Error getting pull request statuses", { error, workspace, repo_slug, pull_request_id, }); throw new McpError( ErrorCode.InternalError, `Failed to get pull request statuses: ${ error instanceof Error ? error.message : String(error) }` ); } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info("Bitbucket MCP server running on stdio"); } } // Create and start the server const server = new BitbucketServer(); server.run().catch((error) => { logger.error("Server error", error); process.exit(1); });

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/MatanYemini/bitbucket-mcp'

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