bb_clone_repo
Clone Bitbucket repositories to your local filesystem using SSH or HTTPS. Specify workspace, repository, and target path for efficient setup and access.
Instructions
Clones a Bitbucket repository to your local filesystem using SSH (preferred) or HTTPS. Requires Bitbucket credentials and proper SSH key setup for optimal usage.
Parameters:
workspaceSlug: The Bitbucket workspace containing the repository (optional - will use default if not provided)repoSlug: The repository name to clone (required)targetPath: Parent directory where repository will be cloned (required)
Path Handling:
Absolute paths are strongly recommended (e.g., "/home/user/projects" or "C:\Users\name\projects")
Relative paths (e.g., "./my-repos" or "../downloads") will be resolved relative to the server's working directory, which may not be what you expect
The repository will be cloned into a subdirectory at
targetPath/repoSlugMake sure you have write permissions to the target directory
SSH Requirements:
SSH keys must be properly configured for Bitbucket
SSH agent should be running with your keys added
Will automatically fall back to HTTPS if SSH is unavailable
Example Usage:
Returns: Success message with clone details or an error message with troubleshooting steps.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| repoSlug | Yes | Repository name/slug to clone. This is the short name of the repository. Example: "project-api" | |
| targetPath | Yes | Directory path where the repository will be cloned. IMPORTANT: Absolute paths are strongly recommended (e.g., "/home/user/projects" or "C:\Users\name\projects"). Relative paths will be resolved relative to the server's working directory, which may not be what you expect. The repository will be cloned into a subdirectory at targetPath/repoSlug. Make sure you have write permissions to this location. | |
| workspaceSlug | No | Bitbucket workspace slug containing the repository. If not provided, the tool will use your default workspace (either configured via BITBUCKET_DEFAULT_WORKSPACE or the first workspace in your account). Example: "myteam" |
Implementation Reference
- src/tools/atlassian.repositories.tool.ts:70-78 (registration)Registers the bb_clone tool (Bitbucket repository cloner) with the MCP server, using handleRepoClone as the handler and CloneRepositoryToolArgs as input schema.server.registerTool( 'bb_clone', { title: 'Clone Bitbucket Repository', description: BB_CLONE_DESCRIPTION, inputSchema: CloneRepositoryToolArgs, }, handleRepoClone, );
- Tool handler wrapper that validates args, calls the controller's handleCloneRepository, formats response for MCP, and handles errors.async function handleRepoClone(args: Record<string, unknown>) { const methodLogger = Logger.forContext( 'tools/atlassian.repositories.tool.ts', 'handleRepoClone', ); try { methodLogger.debug('Cloning repository:', args); // Pass args directly to controller const result = await handleCloneRepository( args as CloneRepositoryToolArgsType, ); methodLogger.debug('Successfully cloned repository via controller'); return { content: [ { type: 'text' as const, text: truncateForAI(result.content, result.rawResponsePath), }, ], }; } catch (error) { methodLogger.error('Failed to clone repository', error); return formatErrorForMcpTool(error); } }
- Core handler implementing the repository cloning: fetches repo details, determines clone URL (SSH preferred), executes git clone command, handles paths/permissions/errors comprehensively.export async function handleCloneRepository( options: CloneRepositoryToolArgsType, ): Promise<ControllerResponse> { 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 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, constants.F_OK); methodLogger.debug( `Target directory exists: ${processedTargetPath}`, ); // If it exists, check if we have write permission try { await fs.access(processedTargetPath, 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 as Error).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 atlassianRepositoriesService.get({ workspace: workspaceSlug, repo_slug: repoSlug, }); // Find SSH clone URL (preferred) or fall back to HTTPS let cloneUrl: string | undefined; let cloneProtocol: string = '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 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 as Error).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 handleControllerError(error, { entityType: 'Repository', operation: 'clone', source: 'controllers/atlassian.repositories.content.controller.ts@handleCloneRepository', additionalInfo: options, }); } }
- Zod schema defining input arguments for the clone repository tool: workspaceSlug (opt), repoSlug (req), targetPath (req).export const CloneRepositoryToolArgs = z.object({ workspaceSlug: z .string() .optional() .describe( 'Bitbucket workspace slug containing the repository. If not provided, the tool will use your default workspace (either configured via BITBUCKET_DEFAULT_WORKSPACE or the first workspace in your account). Example: "myteam"', ), repoSlug: z .string() .min(1, 'Repository slug is required') .describe( 'Repository name/slug to clone. This is the short name of the repository. Example: "project-api"', ), targetPath: z .string() .min(1, 'Target path is required') .describe( 'Directory path where the repository will be cloned. IMPORTANT: Absolute paths are strongly recommended (e.g., "/home/user/projects" or "C:\\Users\\name\\projects"). Relative paths will be resolved relative to the server\'s working directory, which may not be what you expect. The repository will be cloned into a subdirectory at targetPath/repoSlug. Make sure you have write permissions to this location.', ), }); export type CloneRepositoryToolArgsType = z.infer< typeof CloneRepositoryToolArgs