#!/usr/bin/env node /** * Bitbucket Pull Request MCP Server * * Configuration: * 1. Export BITBUCKET_MCP_TOKEN= (environment variable) * 2. Add to .env file in project root * 3. Interactive prompt (development fallback) */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { BitbucketRouter } from './router.js'; const router = new BitbucketRouter(); /** * MCP Server implementation for Bitbucket Pull Requests. */ class BitbucketMCPServer { private server: Server; constructor() { this.server = new Server({ name: 'bitbucket-pullrequests', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // Handle requests this.setupRequestHandlers(); } /** * Setup MCP request handlers. */ private setupRequestHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'validate_token', description: 'Validate the Bitbucket authentication token', inputSchema: { type: 'object', properties: {}, }, }, { name: 'list_pull_requests', description: 'List pull requests in a Bitbucket repository', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, state: { type: 'string', enum: ['open', 'closed', 'all'] }, author: { type: 'string' }, }, }, }, { name: 'get_pull_request', description: 'Get details of a specific pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, }, }, { name: 'get_pull_request_activities', description: 'Get activities/events for a pull request (opens, closes, comments, reviews, etc.)', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, limit: { type: 'integer', description: 'Max results per page (default 25)' }, start: { type: 'integer', description: 'Pagination start index' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_changes', description: 'Get files changed in a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, limit: { type: 'integer', description: 'Max results per page (default 25)' }, start: { type: 'integer', description: 'Pagination start index' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_comments', description: 'Get all comments on a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, limit: { type: 'integer', description: 'Max results per page (default 25)' }, start: { type: 'integer', description: 'Pagination start index' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_comment', description: 'Get a specific comment on a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, commentId: { type: 'integer', description: 'The comment ID' }, }, required: ['workspace', 'repository', 'pullRequestId', 'commentId'], }, }, { name: 'get_pull_request_commits', description: 'Get commits included in a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, limit: { type: 'integer', description: 'Max results per page (default 25)' }, start: { type: 'integer', description: 'Pagination start index' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_diff', description: 'Get the diff of a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, path: { type: 'string', description: 'Specific file path to get diff for' }, context: { type: 'integer', description: 'Number of context lines around changes' }, whitespace: { type: 'string', enum: ['ignore-all', 'ignore-changing', 'ignore-eol', 'show-all'] }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_patch', description: 'Get the raw patch file for a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_participants', description: 'Get participants of a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_reviewers', description: 'Get reviewers of a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_status', description: 'Get status/state of a pull request (open, merged, declined)', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_tasks', description: 'Get tasks associated with a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_pull_request_task_count', description: 'Get count of tasks on a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'get_full_pull_request', description: 'Get full detailed information about a pull request with all fields expanded', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, // ── Repository / workspace ──────────────────────────────────────── { name: 'list_workspaces', description: 'List all Bitbucket workspaces the authenticated user belongs to', inputSchema: { type: 'object', properties: { page: { type: 'integer', description: 'Page number (1-based)' }, pagelen: { type: 'integer', description: 'Results per page (default 10, max 100)' }, }, }, }, { name: 'list_repositories', description: 'List repositories in a Bitbucket workspace', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, role: { type: 'string', enum: ['member', 'contributor', 'owner'] }, page: { type: 'integer' }, pagelen: { type: 'integer' }, }, required: ['workspace'], }, }, { name: 'get_repository', description: 'Get metadata for a specific Bitbucket repository', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, }, required: ['workspace', 'repository'], }, }, { name: 'list_branches', description: 'List branches in a Bitbucket repository', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, filter_by_name: { type: 'string', description: 'Filter branches whose name contains this string' }, page: { type: 'integer' }, pagelen: { type: 'integer' }, }, required: ['workspace', 'repository'], }, }, // ── PR write ────────────────────────────────────────────────────── { name: 'create_pull_request', description: 'Create a new pull request in a Bitbucket repository', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, title: { type: 'string' }, source_branch: { type: 'string' }, destination_branch: { type: 'string' }, description: { type: 'string' }, reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer account UUIDs' }, close_source_branch: { type: 'boolean' }, }, required: ['workspace', 'repository', 'title', 'source_branch', 'destination_branch'], }, }, { name: 'update_pull_request', description: 'Update the title, description, reviewers, or destination branch of a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, title: { type: 'string' }, description: { type: 'string' }, reviewers: { type: 'array', items: { type: 'string' } }, destination_branch: { type: 'string' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'merge_pull_request', description: 'Merge an open pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, merge_strategy: { type: 'string', enum: ['merge_commit', 'squash', 'fast_forward'] }, commit_message: { type: 'string' }, close_source_branch: { type: 'boolean' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'decline_pull_request', description: 'Decline (close) an open pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'approve_pull_request', description: 'Approve a pull request as the authenticated user', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'unapprove_pull_request', description: 'Remove approval from a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'request_changes_pull_request', description: 'Request changes on a pull request as the authenticated user', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, { name: 'remove_request_changes_pull_request', description: 'Remove a request-changes vote from a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId'], }, }, // ── Comment write ───────────────────────────────────────────────── { name: 'add_pull_request_comment', description: 'Add a comment to a pull request (general or inline on a specific file/line)', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, content: { type: 'string', description: 'Comment text (Markdown supported)' }, inline_path: { type: 'string', description: 'File path for inline comment' }, inline_line: { type: 'integer', description: 'Line number for inline comment' }, parent_comment_id: { type: 'integer', description: 'Parent comment ID to reply to' }, }, required: ['workspace', 'repository', 'pullRequestId', 'content'], }, }, { name: 'update_pull_request_comment', description: 'Edit the text of an existing pull request comment', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, commentId: { type: 'integer' }, content: { type: 'string' }, }, required: ['workspace', 'repository', 'pullRequestId', 'commentId', 'content'], }, }, { name: 'delete_pull_request_comment', description: 'Delete a comment from a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, commentId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId', 'commentId'], }, }, // ── Task write ──────────────────────────────────────────────────── { name: 'create_pull_request_task', description: 'Create a review task on a pull request', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, content: { type: 'string', description: 'Task description text' }, comment_id: { type: 'integer', description: 'Anchor task to this comment ID' }, }, required: ['workspace', 'repository', 'pullRequestId', 'content'], }, }, { name: 'update_pull_request_task', description: 'Update a pull request task content or resolve/unresolve it', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, taskId: { type: 'integer' }, content: { type: 'string' }, state: { type: 'string', enum: ['RESOLVED', 'UNRESOLVED'] }, }, required: ['workspace', 'repository', 'pullRequestId', 'taskId'], }, }, { name: 'delete_pull_request_task', description: 'Delete a pull request task', inputSchema: { type: 'object', properties: { workspace: { type: 'string' }, repository: { type: 'string' }, pullRequestId: { type: 'integer' }, taskId: { type: 'integer' }, }, required: ['workspace', 'repository', 'pullRequestId', 'taskId'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: params } = request.params; try { console.log(`Executing tool: ${name}`); console.log('Parameters:', JSON.stringify(params)); const result = await router.executeTool(name, params as any); if (!result.success) { throw new Error(result.error || 'Tool execution failed'); } return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } catch (error) { console.error(`Tool error (${name}):`, error); throw { name: 'ToolExecutionError', message: typeof error === 'string' ? error : (error as Error).message || 'Unknown error', }; } }); } /** * Start the MCP server. */ async start(): Promise { try { const transport = new StdioServerTransport(); await this.server.connect(transport); console.log('✅ Bitbucket MCP Server started'); console.log(' Name: bitbucket-pullrequests v1.0.0'); // Log token source for debugging if (process.env.BITBUCKET_MCP_TOKEN) { console.log(' Token source: environment variable'); } else { console.log(' Token source: .env file or interactive prompt'); } // Validate token on startup try { console.log('🔍 Validating Bitbucket token...'); const router = new BitbucketRouter(); const validation = await router.executeTool('validate_token', {}); if (validation.success) { console.log('✅ Token validation successful'); } else { console.error('❌ Token validation failed:', validation.error); console.error(' Please check your BITBUCKET_MCP_TOKEN configuration'); console.error(' Required permissions: Repository read access'); } } catch (error) { console.error('❌ Token validation error:', error); } } catch (error) { console.error('❌ Server startup failed:', error); process.exit(1); } } } // Entry point const server = new BitbucketMCPServer(); server.start().catch((error) => { console.error('Fatal error:', error); process.exit(1); });