UNPKG

bitbucket-mcp

Version:

Model Context Protocol (MCP) server for Bitbucket Cloud and Server API integration

1,049 lines (1,048 loc) 152 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 from "axios"; import winston from "winston"; import os from "os"; import path from "path"; import fs from "fs"; import { BitbucketPaginator, BITBUCKET_ALL_ITEMS_CAP, BITBUCKET_DEFAULT_PAGELEN, BITBUCKET_MAX_PAGELEN, } from "./pagination.js"; // =========== LOGGER SETUP ========== // File-based logging with sensible defaults and ability to disable function getDefaultLogDirectory() { 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) { if (value === undefined || value === null) return false; const normalized = String(value).toLowerCase(); return ["1", "true", "yes", "on"].includes(normalized); } function getLogFilePath() { 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; if (isTruthyEnv(process.env.BITBUCKET_LOG_PER_CWD)) { const sanitizedCwd = process .cwd() .replace(/[\\/]/g, "_") .replace(/[:*?"<>|]/g, ""); effectiveDir = path.join(baseDir, 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 })] : [], }); const PAGINATION_BASE_SCHEMA = { pagelen: { type: "number", minimum: 1, maximum: BITBUCKET_MAX_PAGELEN, description: `Number of items per page (Bitbucket pagelen). Defaults to ${BITBUCKET_DEFAULT_PAGELEN} and caps at ${BITBUCKET_MAX_PAGELEN}.`, }, page: { type: "number", minimum: 1, description: "Bitbucket page number to fetch (1-based).", }, }; const PAGINATION_ALL_SCHEMA = { type: "boolean", description: `When true (and no page is provided), automatically follows Bitbucket next links to return all items up to ${BITBUCKET_ALL_ITEMS_CAP}.`, }; const LEGACY_LIMIT_SCHEMA = { type: "number", description: "Deprecated alias for pagelen. Use pagelen/page/all for pagination control.", }; // Normalize Bitbucket configuration for backward compatibility and DX function normalizeBitbucketConfig(rawConfig) { 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; } // =========== MCP SERVER =========== class BitbucketServer { isDangerousTool(name) { // Explicitly dangerous or conservative prefix match (delete*) if (this.dangerousToolNames.has(name)) return true; if (/^delete/i.test(name)) return true; return false; } constructor() { this.dangerousToolNames = new Set([ "deletePullRequestComment", "deletePullRequestTask", ]); // 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 = { 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, }); this.paginator = new BitbucketPaginator(this.api, logger); // 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); } 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", }, name: { type: "string", description: "Filter repositories by name (partial match supported)", }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, limit: LEGACY_LIMIT_SCHEMA, }, }, }, { 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", }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, limit: LEGACY_LIMIT_SCHEMA, }, 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 UUIDs (e.g., '{04776764-62c7-453b-b97e-302f60395ceb}')", }, 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", }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, }, 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", }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, }, 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", }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, }, 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", }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, }, 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", }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, }, 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", }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, }, 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 UUIDs (e.g., '{04776764-62c7-453b-b97e-302f60395ceb}')", }, }, 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" }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, limit: LEGACY_LIMIT_SCHEMA, 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", }, ...PAGINATION_BASE_SCHEMA, all: PAGINATION_ALL_SCHEMA, }, 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: {