@gohcltech/bitbucket-mcp
Version:
Bitbucket integration for Claude via Model Context Protocol
477 lines • 20.2 kB
JavaScript
/**
* @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