@aashari/mcp-server-atlassian-bitbucket
Version:
Node.js/TypeScript MCP server for Atlassian Bitbucket. Enables AI systems (LLMs) to interact with workspaces, repositories, and pull requests via tools (list, get, comment, search). Connects AI directly to version control workflows through the standard MC
208 lines (207 loc) • 10.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleCloneRepository = handleCloneRepository;
const vendor_atlassian_repositories_service_js_1 = __importDefault(require("../services/vendor.atlassian.repositories.service.js"));
const logger_util_js_1 = require("../utils/logger.util.js");
const error_handler_util_js_1 = require("../utils/error-handler.util.js");
const workspace_util_js_1 = require("../utils/workspace.util.js");
const shell_util_js_1 = require("../utils/shell.util.js");
const path = __importStar(require("path"));
const fs = __importStar(require("fs/promises"));
const fs_1 = require("fs");
// Logger instance for this module
const logger = logger_util_js_1.Logger.forContext('controllers/atlassian.repositories.content.controller.ts');
/**
* Clones a Bitbucket repository to the local filesystem
* @param options Options including repository identifiers and target path
* @returns Information about the cloned repository
*/
async function handleCloneRepository(options) {
const methodLogger = logger.forMethod('handleCloneRepository');
methodLogger.debug('Cloning repository with options:', options);
try {
// Handle optional workspaceSlug
let { workspaceSlug } = options;
if (!workspaceSlug) {
methodLogger.debug('No workspace provided, fetching default workspace');
const defaultWorkspace = await (0, workspace_util_js_1.getDefaultWorkspace)();
if (!defaultWorkspace) {
throw new Error('No default workspace found. Please provide a workspace slug.');
}
workspaceSlug = defaultWorkspace;
methodLogger.debug(`Using default workspace: ${defaultWorkspace}`);
}
// Required parameters check
const { repoSlug, targetPath } = options;
if (!repoSlug) {
throw new Error('Repository slug is required');
}
if (!targetPath) {
throw new Error('Target path is required');
}
// Normalize and resolve the target path
// If it's a relative path, convert it to absolute based on current working directory
const processedTargetPath = path.isAbsolute(targetPath)
? targetPath
: path.resolve(process.cwd(), targetPath);
methodLogger.debug(`Normalized target path: ${processedTargetPath} (original: ${targetPath})`);
// Validate directory access and permissions before proceeding
try {
// Check if target directory exists
try {
await fs.access(processedTargetPath, fs_1.constants.F_OK);
methodLogger.debug(`Target directory exists: ${processedTargetPath}`);
// If it exists, check if we have write permission
try {
await fs.access(processedTargetPath, fs_1.constants.W_OK);
methodLogger.debug(`Have write permission to: ${processedTargetPath}`);
}
catch {
throw new Error(`Permission denied: You don't have write access to the target directory: ${processedTargetPath}`);
}
}
catch {
// Directory doesn't exist, try to create it
methodLogger.debug(`Target directory doesn't exist, creating: ${processedTargetPath}`);
try {
await fs.mkdir(processedTargetPath, { recursive: true });
methodLogger.debug(`Successfully created directory: ${processedTargetPath}`);
}
catch (mkdirError) {
throw new Error(`Failed to create target directory ${processedTargetPath}: ${mkdirError.message}. Please ensure you have write permissions to the parent directory.`);
}
}
}
catch (accessError) {
methodLogger.error('Path access error:', accessError);
throw accessError;
}
// Get repository details to determine clone URL
methodLogger.debug(`Getting repository details for ${workspaceSlug}/${repoSlug}`);
const repoDetails = await vendor_atlassian_repositories_service_js_1.default.get({
workspace: workspaceSlug,
repo_slug: repoSlug,
});
// Find SSH clone URL (preferred) or fall back to HTTPS
let cloneUrl;
let cloneProtocol = 'SSH'; // Default to SSH
if (repoDetails.links?.clone) {
// First try to find SSH clone URL
const sshClone = repoDetails.links.clone.find((link) => link.name === 'ssh');
if (sshClone) {
cloneUrl = sshClone.href;
}
else {
// Fall back to HTTPS if SSH is not available
const httpsClone = repoDetails.links.clone.find((link) => link.name === 'https');
if (httpsClone) {
cloneUrl = httpsClone.href;
cloneProtocol = 'HTTPS';
methodLogger.warn('SSH clone URL not found, falling back to HTTPS');
}
}
}
if (!cloneUrl) {
throw new Error('Could not find a valid clone URL for the repository');
}
// Determine full target directory path
// Clone into a subdirectory named after the repo slug
const targetDir = path.join(processedTargetPath, repoSlug);
methodLogger.debug(`Will clone to: ${targetDir}`);
// Check if directory already exists
try {
const stats = await fs.stat(targetDir);
if (stats.isDirectory()) {
methodLogger.warn(`Target directory already exists: ${targetDir}`);
return {
content: `Target directory \`${targetDir}\` already exists. Please choose a different target path or remove the existing directory.`,
};
}
}
catch {
// Error means directory doesn't exist, which is what we want
methodLogger.debug(`Target directory doesn't exist, proceeding with clone`);
}
// Execute git clone command
methodLogger.debug(`Cloning from URL (${cloneProtocol}): ${cloneUrl}`);
const command = `git clone ${cloneUrl} "${targetDir}"`;
try {
const result = await (0, shell_util_js_1.executeShellCommand)(command, 'cloning repository');
// Return success message with more detailed information
return {
content: `Successfully cloned repository \`${workspaceSlug}/${repoSlug}\` to \`${targetDir}\` using ${cloneProtocol}.\n\n` +
`**Details:**\n` +
`- **Repository**: ${workspaceSlug}/${repoSlug}\n` +
`- **Clone Protocol**: ${cloneProtocol}\n` +
`- **Target Location**: ${targetDir}\n\n` +
`**Output:**\n\`\`\`\n${result}\n\`\`\`\n\n` +
`**Note**: If this is your first time cloning with SSH, ensure your SSH keys are set up correctly.`,
};
}
catch (cloneError) {
// Enhanced error message with troubleshooting steps
const errorMsg = `Failed to clone repository: ${cloneError.message}`;
let troubleshooting = '';
if (cloneProtocol === 'SSH') {
troubleshooting =
`\n\n**Troubleshooting SSH Clone Issues:**\n` +
`1. Ensure you have SSH keys set up with Bitbucket\n` +
`2. Check if your SSH agent is running: \`eval "$(ssh-agent -s)"; ssh-add\`\n` +
`3. Verify connectivity: \`ssh -T git@bitbucket.org\`\n` +
`4. Try using HTTPS instead (modify your tool call with a different repository URL)`;
}
else {
troubleshooting =
`\n\n**Troubleshooting HTTPS Clone Issues:**\n` +
`1. Check your Bitbucket credentials\n` +
`2. Ensure the target directory is writable\n` +
`3. Try running the command manually to see detailed errors`;
}
throw new Error(errorMsg + troubleshooting);
}
}
catch (error) {
throw (0, error_handler_util_js_1.handleControllerError)(error, {
entityType: 'Repository',
operation: 'clone',
source: 'controllers/atlassian.repositories.content.controller.ts@handleCloneRepository',
additionalInfo: options,
});
}
}