UNPKG

@zhanglc77/bitbucket-mcp-server

Version:

MCP server for Bitbucket API integration - supports both Cloud and Server

308 lines 14 kB
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { isListDirectoryContentArgs, isGetFileContentArgs } from '../types/guards.js'; import * as path from 'path'; export class FileHandlers { apiClient; baseUrl; // Default lines by file extension DEFAULT_LINES_BY_EXT = { '.yml': 200, '.yaml': 200, '.json': 200, // Config files '.md': 300, '.txt': 300, // Docs '.ts': 500, '.js': 500, '.py': 500, // Code '.tsx': 500, '.jsx': 500, '.java': 500, // More code '.log': -100 // Last 100 lines for logs }; constructor(apiClient, baseUrl) { this.apiClient = apiClient; this.baseUrl = baseUrl; } async handleListDirectoryContent(args) { if (!isListDirectoryContentArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for list_directory_content'); } const { workspace, repository, path: dirPath = '', branch } = args; try { let apiPath; let params = {}; let response; if (this.apiClient.getIsServer()) { // Bitbucket Server API apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/browse`; if (dirPath) { apiPath += `/${dirPath}`; } if (branch) { params.at = `refs/heads/${branch}`; } response = await this.apiClient.makeRequest('get', apiPath, undefined, { params }); } else { // Bitbucket Cloud API const branchOrDefault = branch || 'HEAD'; apiPath = `/repositories/${workspace}/${repository}/src/${branchOrDefault}`; if (dirPath) { apiPath += `/${dirPath}`; } response = await this.apiClient.makeRequest('get', apiPath); } // Format the response let contents = []; let actualBranch = branch; if (this.apiClient.getIsServer()) { // Bitbucket Server response const entries = response.children?.values || []; contents = entries.map((entry) => ({ name: entry.path.name, type: entry.type === 'FILE' ? 'file' : 'directory', size: entry.size, path: dirPath ? `${dirPath}/${entry.path.name}` : entry.path.name })); // Get the actual branch from the response if available if (!branch && response.path?.components) { // Server returns default branch info in the response actualBranch = 'default'; } } else { // Bitbucket Cloud response const entries = response.values || []; contents = entries.map((entry) => ({ name: entry.path.split('/').pop() || entry.path, type: entry.type === 'commit_file' ? 'file' : 'directory', size: entry.size, path: entry.path })); // Cloud returns the branch in the response actualBranch = branch || response.commit?.branch || 'main'; } return { content: [ { type: 'text', text: JSON.stringify({ path: dirPath || '/', branch: actualBranch, contents, total_items: contents.length }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `listing directory '${dirPath}' in ${workspace}/${repository}`); } } async handleGetFileContent(args) { if (!isGetFileContentArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for get_file_content'); } const { workspace, repository, file_path, branch, start_line, line_count, full_content = false } = args; try { let fileContent; let fileMetadata = {}; const fileSizeLimit = 1024 * 1024; // 1MB default limit if (this.apiClient.getIsServer()) { // Bitbucket Server - get file metadata first to check size const browsePath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/browse/${file_path}`; const browseParams = {}; if (branch) { browseParams.at = `refs/heads/${branch}`; } try { const metadataResponse = await this.apiClient.makeRequest('get', browsePath, undefined, { params: browseParams }); fileMetadata = { size: metadataResponse.size || 0, path: file_path }; // Check file size if (!full_content && fileMetadata.size > fileSizeLimit) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'File too large', file_path, size: fileMetadata.size, size_mb: (fileMetadata.size / (1024 * 1024)).toFixed(2), message: `File exceeds size limit. Use full_content: true to force retrieval or use start_line/line_count for partial content.` }, null, 2), }, ], isError: true, }; } } catch (e) { // If browse fails, continue to try raw endpoint } // Get raw content const rawPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/raw/${file_path}`; const rawParams = {}; if (branch) { rawParams.at = `refs/heads/${branch}`; } const response = await this.apiClient.makeRequest('get', rawPath, undefined, { params: rawParams, responseType: 'text', headers: { 'Accept': 'text/plain' } }); fileContent = response; } else { // Bitbucket Cloud - first get metadata const branchOrDefault = branch || 'HEAD'; const metaPath = `/repositories/${workspace}/${repository}/src/${branchOrDefault}/${file_path}`; const metadataResponse = await this.apiClient.makeRequest('get', metaPath); fileMetadata = { size: metadataResponse.size, encoding: metadataResponse.encoding, path: metadataResponse.path, commit: metadataResponse.commit }; // Check file size if (!full_content && fileMetadata.size > fileSizeLimit) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'File too large', file_path, size: fileMetadata.size, size_mb: (fileMetadata.size / (1024 * 1024)).toFixed(2), message: `File exceeds size limit. Use full_content: true to force retrieval or use start_line/line_count for partial content.` }, null, 2), }, ], isError: true, }; } // Follow the download link to get actual content const downloadUrl = metadataResponse.links.download.href; const downloadResponse = await this.apiClient.makeRequest('get', downloadUrl, undefined, { baseURL: '', // Use full URL responseType: 'text', headers: { 'Accept': 'text/plain' } }); fileContent = downloadResponse; } // Apply line filtering if requested let processedContent = fileContent; let lineInfo = null; if (!full_content || start_line !== undefined || line_count !== undefined) { const lines = fileContent.split('\n'); const totalLines = lines.length; // Determine default line count based on file extension const ext = path.extname(file_path).toLowerCase(); const defaultLineCount = this.DEFAULT_LINES_BY_EXT[ext] || 500; const shouldUseTail = defaultLineCount < 0; // Calculate start and end indices let startIdx; let endIdx; if (start_line !== undefined) { if (start_line < 0) { // Negative start_line means from end startIdx = Math.max(0, totalLines + start_line); endIdx = totalLines; } else { // 1-based to 0-based index startIdx = Math.max(0, start_line - 1); endIdx = startIdx + (line_count || Math.abs(defaultLineCount)); } } else if (!full_content && fileMetadata.size > 50 * 1024) { // Auto-truncate large files if (shouldUseTail) { startIdx = Math.max(0, totalLines + defaultLineCount); endIdx = totalLines; } else { startIdx = 0; endIdx = Math.abs(defaultLineCount); } } else { // Return full content for small files startIdx = 0; endIdx = totalLines; } // Ensure indices are within bounds startIdx = Math.max(0, Math.min(startIdx, totalLines)); endIdx = Math.max(startIdx, Math.min(endIdx, totalLines)); // Extract the requested lines const selectedLines = lines.slice(startIdx, endIdx); processedContent = selectedLines.join('\n'); lineInfo = { total_lines: totalLines, returned_lines: { start: startIdx + 1, end: endIdx }, truncated: startIdx > 0 || endIdx < totalLines, message: endIdx < totalLines ? `Showing lines ${startIdx + 1}-${endIdx} of ${totalLines}. File size: ${(fileMetadata.size / 1024).toFixed(1)}KB` : null }; } // Build response const response = { file_path, branch: branch || (this.apiClient.getIsServer() ? 'default' : 'main'), size: fileMetadata.size || fileContent.length, encoding: fileMetadata.encoding || 'utf-8', content: processedContent }; if (lineInfo) { response.line_info = lineInfo; } if (fileMetadata.commit) { response.last_modified = { commit_id: fileMetadata.commit.hash, author: fileMetadata.commit.author?.user?.display_name || fileMetadata.commit.author?.raw, date: fileMetadata.commit.date, message: fileMetadata.commit.message }; } return { content: [ { type: 'text', text: JSON.stringify(response, null, 2), }, ], }; } catch (error) { // Handle specific not found error if (error.status === 404) { return { content: [ { type: 'text', text: `File '${file_path}' not found in ${workspace}/${repository}${branch ? ` on branch '${branch}'` : ''}`, }, ], isError: true, }; } return this.apiClient.handleApiError(error, `getting file content for '${file_path}' in ${workspace}/${repository}`); } } // Helper method to get default line count based on file extension getDefaultLines(filePath, fileSize) { // Small files: return full content if (fileSize < 50 * 1024) { // 50KB return { full: true }; } const ext = path.extname(filePath).toLowerCase(); const defaultLines = this.DEFAULT_LINES_BY_EXT[ext] || 500; return { start: defaultLines < 0 ? defaultLines : 1, count: Math.abs(defaultLines) }; } } //# sourceMappingURL=file-handlers.js.map