610 lines
23 KiB
JavaScript
610 lines
23 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Bitbucket Pull Request MCP Server
|
|
*
|
|
* Configuration:
|
|
* 1. Export BITBUCKET_MCP_TOKEN=<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<void> {
|
|
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);
|
|
}); |