UNPKG

@gohcltech/bitbucket-mcp

Version:

Bitbucket integration for Claude via Model Context Protocol

477 lines 20.2 kB
#!/usr/bin/env node /** * @fileoverview Main entry point for the Bitbucket MCP (Model Context Protocol) Server. * * This file orchestrates the initialization and management of a full-featured MCP server * that enables Claude to interact with Bitbucket repositories through a standardized interface. * The server provides comprehensive token-based authentication, rate limiting, error handling, * and modular tool architecture for Bitbucket operations. * * Key features: * - Token-based authentication (API Token or Repository Token) * - Rate limiting to respect Bitbucket API limits * - Comprehensive error handling and retry logic * - Modular tool architecture for maintainability * - Full workspace, repository, branch, and pull request management * */ // Load environment variables from .env file import dotenv from 'dotenv'; import path from 'path'; import { fileURLToPath } from 'url'; // Get the directory of this file to locate .env const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const envPath = path.resolve(__dirname, '..', '.env'); dotenv.config({ path: envPath }); import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { BitbucketClient } from './clients/index.js'; import { createAuthService } from './auth-service.js'; import { validateAuthenticationToken } from './token-validator.js'; import { createLogger, gracefulShutdown as loggerShutdown } from './logger.js'; import { createErrorHandler } from './error-handler.js'; import { createRateLimiter } from './rate-limiter.js'; import { ConfigManager } from './config.js'; import { ManageWorkspacesTool, ManageRepositoriesTool, ManageBranchesTool, ManagePullRequestsTool, QueryCommitsTool, ManageIssuesTool, } from './tools/index.js'; /** * Main MCP server class that orchestrates all Bitbucket operations. * * This class serves as the central coordinator for the Bitbucket MCP server, managing: * - Server lifecycle (initialization, startup, shutdown) * - Service orchestration (Authentication, HTTP client) * - Tool module registration and delegation * - Request routing and authentication enforcement * - Error handling and logging * * The server implements a modular architecture where different categories of operations * (workspaces, repositories, branches, pull requests) are handled by specialized * tool modules that extend the BaseTool class. * * @example * ```typescript * const server = new BitbucketMCPServer(); * await server.run(); * ``` */ class BitbucketMCPServer { /** The core MCP server instance that handles protocol communication */ server; /** HTTP client for making authenticated requests to Bitbucket API */ bitbucketClient; /** Authentication service for token management */ authService; /** Authentication capabilities for determining available tools */ authCapabilities; /** Array of tool modules that provide Bitbucket operations */ toolModules = []; /** Map of tool names to their handler functions for quick lookup */ toolHandlers = new Map(); /** Configuration manager for server and authentication settings */ configManager; /** Flag indicating whether the server has completed initialization */ isInitialized = false; /** Logger instance for structured logging throughout the server */ logger; /** * Constructs a new Bitbucket MCP Server instance. * * Initializes the core MCP server with basic capabilities and starts the * asynchronous initialization of all required services. The server is not * ready to handle requests until initialization completes. * * @throws {Error} If configuration validation fails or required environment variables are missing */ constructor() { // Initialize configuration first to get version this.configManager = ConfigManager.getInstance(); const serverConfig = this.configManager.getServerConfig(); this.server = new Server({ name: 'bitbucket-mcp', version: serverConfig.version, // Use dynamic version from package.json }, { capabilities: { tools: {}, }, }); // Initialize services this.initializeServices(); } /** * Initializes all server services and validates configuration. * * This method performs the complete server initialization sequence: * 1. Validates configuration against schema requirements * 2. Initializes logging with appropriate level * 3. Sets up error handling and rate limiting * 4. Initializes authentication service * 5. Validates authentication token * 6. Creates Bitbucket client * 7. Registers all tool modules and their handlers * * @private * @throws {Error} If any service fails to initialize or configuration is invalid */ async initializeServices() { try { // Validate configuration const validation = this.configManager.validateConfig(); if (!validation.isValid) { console.error('Configuration validation failed:'); validation.errors.forEach(error => console.error(` - ${error}`)); process.exit(1); } const serverConfig = this.configManager.getServerConfig(); const authConfig = this.configManager.getAuthConfig(); // Initialize logger this.logger = createLogger({ level: serverConfig.logLevel, service: 'bitbucket-mcp', version: serverConfig.version, logging: serverConfig.logging, }); this.logger.info('Initializing Bitbucket MCP Server', { operation: 'server_init', config: this.configManager.getConfigSummary(), }); // Initialize error handler createErrorHandler({ maxRetries: 3, baseDelay: 1000, maxDelay: 30000, backoffFactor: 2, }); // Initialize rate limiter createRateLimiter(serverConfig.rateLimit); // Initialize authentication service this.authService = createAuthService(authConfig); // Get authentication capabilities for tool filtering this.authCapabilities = this.authService.getCapabilities(); this.logger.info('Authentication service initialized', { operation: 'auth_service_init', method: this.authService.getAuthMethod(), authMethod: this.authService.getAuthMethod(), availableCapabilities: this.authCapabilities.getAvailableCapabilities(), }); // Validate authentication token this.logger.info('Validating authentication token', { operation: 'token_validation', }); const validationResult = await validateAuthenticationToken(this.authService); if (!validationResult.isValid) { this.logger.error('Token validation failed', undefined, { operation: 'token_validation_failed', error: validationResult.errorMessage, errorCode: validationResult.errorCode, }); throw new Error(`Token validation failed: ${validationResult.errorMessage}`); } this.logger.info('Authentication token validated successfully', { operation: 'token_validation_success', }); // Initialize Bitbucket client this.bitbucketClient = new BitbucketClient(this.authService); // Initialize tool modules await this.initializeToolModules(); // Setup tool handlers this.setupToolHandlers(); this.isInitialized = true; this.logger.info('Bitbucket MCP Server initialized successfully', { operation: 'server_init_complete', toolCount: this.toolHandlers.size, isAuthenticated: true, }); } catch (error) { console.error('Failed to initialize Bitbucket MCP Server:', error); if (this.logger) { this.logger.error('Server initialization failed', error, { operation: 'server_init_error', }); } // Provide helpful error messages if (error instanceof Error) { if (error.message.includes('Authentication required')) { console.error('\nPlease set one of the following authentication methods:'); console.error('\nMethod 1: API Token (username + token)'); console.error(' BITBUCKET_USERNAME - Your Bitbucket username'); console.error(' BITBUCKET_API_TOKEN - Your API token (starts with ATBBT)'); console.error('\nMethod 2: Repository Access Token'); console.error(' BITBUCKET_REPOSITORY_ACCESS_TOKEN - Repository access token'); console.error('\nOptional:'); console.error(' BITBUCKET_WORKSPACE - Default workspace to use'); } } process.exit(1); } } /** * Initializes and registers all tool modules. * * Creates instances of each tool module category (Workspace, Repository, Branch, * PullRequest, Commit, and Issue tools) and collects their handler functions into a * central registry for request routing. Filters tool modules based on authentication * capabilities to ensure only available tools are registered. * * @private * @throws {Error} If BitbucketClient is not initialized */ async initializeToolModules() { if (!this.bitbucketClient) { throw new Error('BitbucketClient not initialized'); } if (!this.authCapabilities) { throw new Error('Authentication capabilities not initialized'); } this.logger.debug('Initializing tool modules', { operation: 'tool_modules_init', }); // Initialize all tool modules const allToolModules = [ new ManageWorkspacesTool(this.bitbucketClient), new ManageRepositoriesTool(this.bitbucketClient), new ManageBranchesTool(this.bitbucketClient), new ManagePullRequestsTool(this.bitbucketClient), new QueryCommitsTool(this.bitbucketClient), new ManageIssuesTool(this.bitbucketClient), ]; // Filter tool modules based on authentication capabilities this.toolModules = []; const filteredOutTools = []; for (const toolModule of allToolModules) { const category = toolModule.getToolCategory(); if (this.authCapabilities.isToolCategoryAvailable(category)) { this.toolModules.push(toolModule); } else { const reason = this.authCapabilities.getUnavailabilityReason(category); filteredOutTools.push(`${category} (${reason})`); } } // Log if tools were filtered out if (filteredOutTools.length > 0) { this.logger.info('Tool categories filtered out due to insufficient capabilities', { operation: 'tool_modules_capability_filter', filteredCategories: filteredOutTools, authMethod: this.authService?.getAuthMethod(), }); } // Collect all handlers from available tool modules this.toolHandlers.clear(); for (const toolModule of this.toolModules) { const handlers = toolModule.getHandlers(); for (const [name, handler] of handlers) { if (this.toolHandlers.has(name)) { this.logger.warn(`Duplicate tool handler: ${name}`, { operation: 'tool_modules_init', tool: name, }); } this.toolHandlers.set(name, handler); } } this.logger.info('Tool modules initialized', { operation: 'tool_modules_init_complete', totalModules: allToolModules.length, availableModules: this.toolModules.length, filteredModules: filteredOutTools.length, toolCount: this.toolHandlers.size, }); } /** * Sets up MCP protocol handlers for tool operations. * * Registers handlers for the two core MCP operations: * - ListTools: Returns all available tools from registered modules * - CallTool: Executes a specific tool with error handling * * @private */ setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { try { if (!this.isInitialized) { throw new Error('Server not fully initialized'); } const allTools = []; // Collect tools from all modules for (const toolModule of this.toolModules) { allTools.push(...toolModule.getTools()); } this.logger.debug('Listed tools', { operation: 'list_tools', toolCount: allTools.length, }); return { tools: allTools, }; } catch (error) { this.logger.error('Error listing tools', error, { operation: 'list_tools_error', }); return { tools: [], }; } }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; const startTime = Date.now(); try { if (!this.isInitialized) { throw new Error('Server not fully initialized'); } this.logger.debug(`Tool call: ${name}`, { operation: 'tool_call', tool: name, args: this.sanitizeArgsForLogging(args), }); // Find and execute the appropriate handler const handler = this.toolHandlers.get(name); if (!handler) { throw new Error(`Unknown tool: ${name}`); } const result = await handler(args); const duration = Date.now() - startTime; this.logger.info(`Tool completed: ${name}`, { operation: 'tool_complete', tool: name, duration, }); return result; } catch (error) { const duration = Date.now() - startTime; this.logger.error(`Tool failed: ${name}`, error, { operation: 'tool_error', tool: name, duration, args: this.sanitizeArgsForLogging(args), }); return { content: [ { type: 'text', text: `Error in ${name}: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); } /** * Sanitizes tool arguments for safe logging. * * Removes sensitive information from tool arguments before logging to prevent * credential exposure in log files. Scans for common sensitive field names * and replaces their values with '[REDACTED]' placeholder. * * @private * @param args - Tool arguments to sanitize * @returns Sanitized arguments object safe for logging */ sanitizeArgsForLogging(args) { if (!args || typeof args !== 'object') { return args; } const sanitized = { ...args }; const sensitiveKeys = ['token', 'password', 'secret', 'key', 'authorization', 'code']; for (const key of Object.keys(sanitized)) { if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) { sanitized[key] = '[REDACTED]'; } } return sanitized; } /** * Starts the MCP server and begins listening for requests. * * This method: * 1. Waits for initialization to complete * 2. Creates a stdio transport for MCP communication * 3. Connects the server to the transport * 4. Sets up graceful shutdown handlers for SIGINT and SIGTERM * 5. Performs cleanup on shutdown (stops rate limiter, etc.) * * The server runs indefinitely until a shutdown signal is received. * * @throws {Error} If server fails to start or connect to transport */ async run() { try { // Wait for initialization to complete while (!this.isInitialized) { await new Promise(resolve => setTimeout(resolve, 100)); } const transport = new StdioServerTransport(); await this.server.connect(transport); this.logger.info('Bitbucket MCP Server started successfully', { operation: 'server_start', transport: 'stdio', }); // Set up graceful shutdown const gracefulShutdown = async (signal) => { this.logger.info(`Received ${signal}, shutting down gracefully`, { operation: 'server_shutdown', signal, }); try { // Perform cleanup const rateLimiter = await import('./rate-limiter.js').then(m => m.getRateLimiter()); await rateLimiter.stop(); // Close logger and transports await loggerShutdown(); console.log('Server shutdown complete'); process.exit(0); } catch (error) { console.error('Error during shutdown:', error); process.exit(1); } }; process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); } catch (error) { if (this.logger) { this.logger.error('Server failed to start', error, { operation: 'server_start_error', }); } else { console.error('Server failed to start:', error); } process.exit(1); } } } /** * Global error handlers for unhandled exceptions and promise rejections. * * These handlers ensure the process exits cleanly when encountering * unexpected errors that could leave the server in an unstable state. */ process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); }); /** * Application entry point. * * Creates and starts the Bitbucket MCP server instance. The server will * initialize all services, register tool handlers, and begin listening * for MCP requests via stdio transport. */ const server = new BitbucketMCPServer(); server.run().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); //# sourceMappingURL=index.js.map