UNPKG

@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

376 lines (375 loc) 15.8 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startServer = startServer; const node_crypto_1 = require("node:crypto"); const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const logger_util_js_1 = require("./utils/logger.util.js"); const config_util_js_1 = require("./utils/config.util.js"); const constants_util_js_1 = require("./utils/constants.util.js"); const index_js_1 = require("./cli/index.js"); const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); // Import tools const atlassian_api_tool_js_1 = __importDefault(require("./tools/atlassian.api.tool.js")); const atlassian_repositories_tool_js_1 = __importDefault(require("./tools/atlassian.repositories.tool.js")); // Create a contextualized logger for this file const indexLogger = logger_util_js_1.Logger.forContext('index.ts'); // Log initialization at debug level indexLogger.debug('Bitbucket MCP server module loaded'); let serverInstance = null; let transportInstance = null; let httpServerInstance = null; const httpSessions = new Map(); function createMcpServer() { const server = new mcp_js_1.McpServer({ name: constants_util_js_1.PACKAGE_NAME, version: constants_util_js_1.VERSION, }); atlassian_api_tool_js_1.default.registerTools(server); atlassian_repositories_tool_js_1.default.registerTools(server); return server; } function getSessionId(req) { const rawSessionId = req.headers['mcp-session-id']; if (Array.isArray(rawSessionId)) { return rawSessionId[0] ?? null; } if (typeof rawSessionId === 'string' && rawSessionId.length > 0) { return rawSessionId; } return null; } /** * Start the MCP server with the specified transport mode * * @param mode The transport mode to use (stdio or http) * @returns Promise that resolves to the server instance when started successfully */ async function startServer(mode = 'stdio') { indexLogger.info(`Starting MCP server in ${mode} mode with ${constants_util_js_1.PACKAGE_NAME} v${constants_util_js_1.VERSION}`); const serverLogger = logger_util_js_1.Logger.forContext('index.ts', 'startServer'); // Load configuration serverLogger.info('Starting MCP server initialization...'); config_util_js_1.config.load(); if (config_util_js_1.config.getBoolean('DEBUG')) { serverLogger.debug('Debug mode enabled'); } if (mode === 'stdio') { serverLogger.info(`Initializing Bitbucket MCP server v${constants_util_js_1.VERSION}`); serverLogger.info('Registering MCP tools...'); serverInstance = createMcpServer(); serverLogger.info('All tools registered successfully'); // STDIO Transport serverLogger.info('Using STDIO transport for MCP communication'); transportInstance = new stdio_js_1.StdioServerTransport(); try { await serverInstance.connect(transportInstance); serverLogger.info('MCP server started successfully on STDIO transport'); setupGracefulShutdown(); return serverInstance; } catch (err) { serverLogger.error('Failed to start server on STDIO transport', err); process.exit(1); } } else { // HTTP Transport with Express serverLogger.info('Using Streamable HTTP transport for MCP communication'); const app = (0, express_1.default)(); // DNS rebinding protection — validate Origin header app.use((req, res, next) => { const origin = req.headers.origin; if (!origin) { return next(); } const allowedOrigins = [ 'http://localhost', 'http://127.0.0.1', 'http://[::1]', 'https://localhost', 'https://127.0.0.1', 'https://[::1]', ]; const isAllowed = allowedOrigins.some((allowed) => origin === allowed || origin.startsWith(`${allowed}:`)); if (!isAllowed) { serverLogger.warn(`Rejected request with invalid origin: ${origin}`); res.status(403).json({ error: 'Forbidden', message: 'Invalid origin for MCP server', }); return; } next(); }); app.use((0, cors_1.default)({ origin: true })); app.use(express_1.default.json({ limit: '1mb' })); const mcpEndpoint = '/mcp'; serverLogger.debug(`MCP endpoint: ${mcpEndpoint}`); // Handle MCP POST requests (initialize + normal RPC calls) app.post(mcpEndpoint, async (req, res) => { const sessionId = getSessionId(req); try { if (sessionId) { const session = httpSessions.get(sessionId); if (!session) { res.status(404).json({ jsonrpc: '2.0', error: { code: -32001, message: 'Session not found for provided Mcp-Session-Id', }, id: null, }); return; } session.lastActivity = Date.now(); await session.transport.handleRequest(req, res, req.body); return; } if (!(0, types_js_1.isInitializeRequest)(req.body)) { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: Missing Mcp-Session-Id header', }, id: null, }); return; } serverLogger.info('Creating new HTTP MCP session for initialize request'); let initializedSessionId = null; const sessionServer = createMcpServer(); const sessionTransport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(), onsessioninitialized: (newSessionId) => { initializedSessionId = newSessionId; httpSessions.set(newSessionId, { server: sessionServer, transport: sessionTransport, lastActivity: Date.now(), }); serverLogger.info(`Initialized HTTP MCP session ${newSessionId}`); }, onsessionclosed: (closedSessionId) => { const session = httpSessions.get(closedSessionId); if (!session) { return; } httpSessions.delete(closedSessionId); void session.server.close().catch((err) => { serverLogger.error(`Error closing server for session ${closedSessionId}`, err); }); serverLogger.info(`Closed HTTP MCP session ${closedSessionId}`); }, }); await sessionServer.connect(sessionTransport); try { await sessionTransport.handleRequest(req, res, req.body); } catch (err) { if (initializedSessionId) { httpSessions.delete(initializedSessionId); } await sessionTransport.close(); await sessionServer.close(); throw err; } } catch (err) { serverLogger.error('Error in HTTP MCP handler', err); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); // Handle optional GET requests for streamable HTTP SSE app.get(mcpEndpoint, async (req, res) => { const sessionId = getSessionId(req); if (!sessionId) { res.status(400).send('Missing Mcp-Session-Id header'); return; } const session = httpSessions.get(sessionId); if (!session) { res.status(404).send('Session not found'); return; } try { await session.transport.handleRequest(req, res); } catch (err) { serverLogger.error('Error in HTTP MCP GET handler', err); if (!res.headersSent) { res.status(500).send('Internal Server Error'); } } }); // Handle session termination requests app.delete(mcpEndpoint, async (req, res) => { const sessionId = getSessionId(req); if (!sessionId) { res.status(400).send('Missing Mcp-Session-Id header'); return; } const session = httpSessions.get(sessionId); if (!session) { res.status(404).send('Session not found'); return; } try { await session.transport.handleRequest(req, res); } catch (err) { serverLogger.error('Error in HTTP MCP DELETE handler', err); if (!res.headersSent) { res.status(500).send('Internal Server Error'); } } }); // Health check endpoint app.get('/', (_req, res) => { res.send(`Bitbucket MCP Server v${constants_util_js_1.VERSION} is running`); }); // Start HTTP server const PORT = Number(process.env.PORT ?? 3000); const HOST = '127.0.0.1'; // Explicit localhost binding for security await new Promise((resolve) => { httpServerInstance = app.listen(PORT, HOST, () => { serverLogger.info(`HTTP transport listening on http://${HOST}:${PORT}${mcpEndpoint}`); serverLogger.info('Server bound to localhost only for security'); resolve(); }); }); // Reap idle HTTP sessions to prevent memory leaks const SESSION_TTL_MS = 30 * 60 * 1000; const REAP_INTERVAL_MS = 5 * 60 * 1000; const reapInterval = setInterval(() => { const now = Date.now(); for (const [sessionId, session] of httpSessions.entries()) { if (now - session.lastActivity > SESSION_TTL_MS) { serverLogger.info(`Reaping idle HTTP session ${sessionId}`); httpSessions.delete(sessionId); void session.transport .close() .catch((err) => serverLogger.debug(`Error closing transport for reaped session ${sessionId}`, err)) .then(() => session.server.close()) .catch((err) => serverLogger.debug(`Error closing server for reaped session ${sessionId}`, err)); } } }, REAP_INTERVAL_MS); reapInterval.unref(); setupGracefulShutdown(); // HTTP mode uses per-session servers (managed in httpSessions map). // Return a reference server for API compatibility; not connected to any transport. return createMcpServer(); } } /** * Main entry point - this will run when executed directly * Determines whether to run in CLI or server mode based on command-line arguments */ async function main() { const mainLogger = logger_util_js_1.Logger.forContext('index.ts', 'main'); // Load configuration config_util_js_1.config.load(); // CLI mode - if any arguments are provided if (process.argv.length > 2) { mainLogger.info('Starting in CLI mode'); await (0, index_js_1.runCli)(process.argv.slice(2)); mainLogger.info('CLI execution completed'); return; } // Server mode - determine transport const transportMode = (process.env.TRANSPORT_MODE || 'stdio').toLowerCase(); let mode; if (transportMode === 'stdio') { mode = 'stdio'; } else if (transportMode === 'http') { mode = 'http'; } else { mainLogger.warn(`Unknown TRANSPORT_MODE "${transportMode}", defaulting to stdio`); mode = 'stdio'; } mainLogger.info(`Starting server with ${mode.toUpperCase()} transport`); await startServer(mode); mainLogger.info('Server is now running'); } /** * Set up graceful shutdown handlers for the server */ function setupGracefulShutdown() { const shutdownLogger = logger_util_js_1.Logger.forContext('index.ts', 'shutdown'); let shuttingDown = false; const shutdown = async () => { if (shuttingDown) return; shuttingDown = true; try { shutdownLogger.info('Shutting down gracefully...'); if (httpSessions.size > 0) { shutdownLogger.info(`Closing ${httpSessions.size} active HTTP session(s)`); } for (const [sessionId, session] of httpSessions.entries()) { try { await session.transport.close(); } catch (err) { shutdownLogger.error(`Error closing transport for session ${sessionId}`, err); } try { await session.server.close(); } catch (err) { shutdownLogger.error(`Error closing server for session ${sessionId}`, err); } } httpSessions.clear(); if (httpServerInstance) { await new Promise((resolve) => { httpServerInstance.close(() => resolve()); }); } if (transportInstance && 'close' in transportInstance && typeof transportInstance.close === 'function') { await transportInstance.close(); } if (serverInstance && typeof serverInstance.close === 'function') { await serverInstance.close(); } process.exit(0); } catch (err) { shutdownLogger.error('Error during shutdown', err); process.exit(1); } }; ['SIGINT', 'SIGTERM'].forEach((signal) => { process.on(signal, shutdown); }); } // If this file is being executed directly (not imported), run the main function if (require.main === module) { main().catch((err) => { indexLogger.error('Unhandled error in main process', err); process.exit(1); }); }