mcp-server-bitbucket 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Bitbucket MCP Server - TypeScript Implementation
4
+ *
5
+ * Provides tools for interacting with Bitbucket repositories,
6
+ * pull requests, pipelines, branches, commits, deployments, and webhooks.
7
+ */
8
+
9
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import {
12
+ CallToolRequestSchema,
13
+ ListToolsRequestSchema,
14
+ ListResourcesRequestSchema,
15
+ ReadResourceRequestSchema,
16
+ ListPromptsRequestSchema,
17
+ GetPromptRequestSchema,
18
+ } from '@modelcontextprotocol/sdk/types.js';
19
+
20
+ import { getSettings } from './settings.js';
21
+ import { toolDefinitions, handleToolCall } from './tools/index.js';
22
+ import { resourceDefinitions, handleResourceRead } from './resources.js';
23
+ import { promptDefinitions, handlePromptGet } from './prompts.js';
24
+
25
+ const VERSION = '0.10.0';
26
+
27
+ function createServer(): Server {
28
+ const server = new Server(
29
+ {
30
+ name: 'bitbucket',
31
+ version: VERSION,
32
+ },
33
+ {
34
+ capabilities: {
35
+ tools: {},
36
+ resources: {},
37
+ prompts: {},
38
+ },
39
+ }
40
+ );
41
+
42
+ // List available tools
43
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
44
+ return {
45
+ tools: toolDefinitions,
46
+ };
47
+ });
48
+
49
+ // Handle tool calls
50
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
51
+ const { name, arguments: args } = request.params;
52
+
53
+ try {
54
+ const result = await handleToolCall(name, args || {});
55
+ return {
56
+ content: [
57
+ {
58
+ type: 'text',
59
+ text: JSON.stringify(result, null, 2),
60
+ },
61
+ ],
62
+ };
63
+ } catch (error) {
64
+ const message = error instanceof Error ? error.message : 'Unknown error';
65
+ return {
66
+ content: [
67
+ {
68
+ type: 'text',
69
+ text: JSON.stringify({ error: message }, null, 2),
70
+ },
71
+ ],
72
+ isError: true,
73
+ };
74
+ }
75
+ });
76
+
77
+ // List available resources
78
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
79
+ return {
80
+ resources: resourceDefinitions,
81
+ };
82
+ });
83
+
84
+ // Handle resource reads
85
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
86
+ const { uri } = request.params;
87
+
88
+ try {
89
+ const content = await handleResourceRead(uri);
90
+ return {
91
+ contents: [
92
+ {
93
+ uri,
94
+ mimeType: 'text/markdown',
95
+ text: content,
96
+ },
97
+ ],
98
+ };
99
+ } catch (error) {
100
+ const message = error instanceof Error ? error.message : 'Unknown error';
101
+ throw new Error(`Failed to read resource: ${message}`);
102
+ }
103
+ });
104
+
105
+ // List available prompts
106
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
107
+ return {
108
+ prompts: promptDefinitions,
109
+ };
110
+ });
111
+
112
+ // Handle prompt gets
113
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
114
+ const { name, arguments: args } = request.params;
115
+
116
+ try {
117
+ const result = handlePromptGet(name, args || {});
118
+ return result;
119
+ } catch (error) {
120
+ const message = error instanceof Error ? error.message : 'Unknown error';
121
+ throw new Error(`Failed to get prompt: ${message}`);
122
+ }
123
+ });
124
+
125
+ return server;
126
+ }
127
+
128
+ async function main(): Promise<void> {
129
+ // Validate settings on startup
130
+ try {
131
+ getSettings();
132
+ } catch (error) {
133
+ console.error('Configuration error:', error instanceof Error ? error.message : error);
134
+ process.exit(1);
135
+ }
136
+
137
+ const server = createServer();
138
+ const transport = new StdioServerTransport();
139
+
140
+ await server.connect(transport);
141
+
142
+ // Log to stderr so it doesn't interfere with MCP communication on stdout
143
+ console.error(`Bitbucket MCP Server v${VERSION} started`);
144
+ }
145
+
146
+ main().catch((error) => {
147
+ console.error('Fatal error:', error);
148
+ process.exit(1);
149
+ });
package/src/prompts.ts ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * MCP Prompts for Bitbucket Server
3
+ */
4
+
5
+ import { Prompt, GetPromptResult, PromptMessage } from '@modelcontextprotocol/sdk/types.js';
6
+
7
+ /**
8
+ * Prompt definitions for the MCP server
9
+ */
10
+ export const promptDefinitions: Prompt[] = [
11
+ {
12
+ name: 'code_review',
13
+ description: 'Generate a code review prompt for a pull request',
14
+ arguments: [
15
+ {
16
+ name: 'repo_slug',
17
+ description: 'Repository slug',
18
+ required: true,
19
+ },
20
+ {
21
+ name: 'pr_id',
22
+ description: 'Pull request ID',
23
+ required: true,
24
+ },
25
+ ],
26
+ },
27
+ {
28
+ name: 'release_notes',
29
+ description: 'Generate release notes from commits between two refs',
30
+ arguments: [
31
+ {
32
+ name: 'repo_slug',
33
+ description: 'Repository slug',
34
+ required: true,
35
+ },
36
+ {
37
+ name: 'base_tag',
38
+ description: 'Base tag or commit (e.g., "v1.0.0")',
39
+ required: true,
40
+ },
41
+ {
42
+ name: 'head',
43
+ description: 'Head ref (default: "main")',
44
+ required: false,
45
+ },
46
+ ],
47
+ },
48
+ {
49
+ name: 'pipeline_debug',
50
+ description: 'Debug a failed pipeline',
51
+ arguments: [
52
+ {
53
+ name: 'repo_slug',
54
+ description: 'Repository slug',
55
+ required: true,
56
+ },
57
+ ],
58
+ },
59
+ {
60
+ name: 'repo_summary',
61
+ description: 'Get a comprehensive summary of a repository',
62
+ arguments: [
63
+ {
64
+ name: 'repo_slug',
65
+ description: 'Repository slug',
66
+ required: true,
67
+ },
68
+ ],
69
+ },
70
+ ];
71
+
72
+ /**
73
+ * Handle prompt get requests
74
+ */
75
+ export function handlePromptGet(
76
+ name: string,
77
+ args: Record<string, string>
78
+ ): GetPromptResult {
79
+ switch (name) {
80
+ case 'code_review':
81
+ return promptCodeReview(args.repo_slug, args.pr_id);
82
+ case 'release_notes':
83
+ return promptReleaseNotes(args.repo_slug, args.base_tag, args.head || 'main');
84
+ case 'pipeline_debug':
85
+ return promptPipelineDebug(args.repo_slug);
86
+ case 'repo_summary':
87
+ return promptRepoSummary(args.repo_slug);
88
+ default:
89
+ throw new Error(`Unknown prompt: ${name}`);
90
+ }
91
+ }
92
+
93
+ function promptCodeReview(repoSlug: string, prId: string): GetPromptResult {
94
+ const content = `Please review pull request #${prId} in repository '${repoSlug}'.
95
+
96
+ Use the following tools to gather information:
97
+ 1. get_pull_request(repo_slug="${repoSlug}", pr_id=${prId}) - Get PR details
98
+ 2. get_pr_diff(repo_slug="${repoSlug}", pr_id=${prId}) - Get the code changes
99
+ 3. list_pr_comments(repo_slug="${repoSlug}", pr_id=${prId}) - See existing comments
100
+
101
+ Then provide a thorough code review covering:
102
+ - Code quality and readability
103
+ - Potential bugs or edge cases
104
+ - Security concerns
105
+ - Performance considerations
106
+ - Suggestions for improvement
107
+
108
+ If you find issues, use add_pr_comment() to leave feedback on specific lines.`;
109
+
110
+ return {
111
+ messages: [
112
+ {
113
+ role: 'user',
114
+ content: {
115
+ type: 'text',
116
+ text: content,
117
+ },
118
+ },
119
+ ],
120
+ };
121
+ }
122
+
123
+ function promptReleaseNotes(repoSlug: string, baseTag: string, head: string): GetPromptResult {
124
+ const content = `Generate release notes for repository '${repoSlug}' comparing ${baseTag} to ${head}.
125
+
126
+ Use these tools:
127
+ 1. compare_commits(repo_slug="${repoSlug}", base="${baseTag}", head="${head}") - See changed files
128
+ 2. list_commits(repo_slug="${repoSlug}", branch="${head}", limit=50) - Get recent commits
129
+
130
+ Organize the release notes into sections:
131
+ - **New Features**: New functionality added
132
+ - **Bug Fixes**: Issues that were resolved
133
+ - **Improvements**: Enhancements to existing features
134
+ - **Breaking Changes**: Changes that require user action
135
+
136
+ Format as markdown suitable for a GitHub/Bitbucket release.`;
137
+
138
+ return {
139
+ messages: [
140
+ {
141
+ role: 'user',
142
+ content: {
143
+ type: 'text',
144
+ text: content,
145
+ },
146
+ },
147
+ ],
148
+ };
149
+ }
150
+
151
+ function promptPipelineDebug(repoSlug: string): GetPromptResult {
152
+ const content = `Help debug pipeline failures in repository '${repoSlug}'.
153
+
154
+ Use these tools:
155
+ 1. list_pipelines(repo_slug="${repoSlug}", limit=5) - Get recent pipeline runs
156
+ 2. get_pipeline(repo_slug="${repoSlug}", pipeline_uuid="<uuid>") - Get pipeline details
157
+ 3. get_pipeline_logs(repo_slug="${repoSlug}", pipeline_uuid="<uuid>") - Get step list
158
+ 4. get_pipeline_logs(repo_slug="${repoSlug}", pipeline_uuid="<uuid>", step_uuid="<step>") - Get logs
159
+
160
+ Analyze the failures and provide:
161
+ - Root cause of the failure
162
+ - Specific error messages
163
+ - Recommended fixes
164
+ - Commands to re-run the pipeline if appropriate`;
165
+
166
+ return {
167
+ messages: [
168
+ {
169
+ role: 'user',
170
+ content: {
171
+ type: 'text',
172
+ text: content,
173
+ },
174
+ },
175
+ ],
176
+ };
177
+ }
178
+
179
+ function promptRepoSummary(repoSlug: string): GetPromptResult {
180
+ const content = `Provide a comprehensive summary of repository '${repoSlug}'.
181
+
182
+ Gather information using:
183
+ 1. get_repository(repo_slug="${repoSlug}") - Basic repo info
184
+ 2. list_branches(repo_slug="${repoSlug}", limit=10) - Active branches
185
+ 3. list_pull_requests(repo_slug="${repoSlug}", state="OPEN") - Open PRs
186
+ 4. list_pipelines(repo_slug="${repoSlug}", limit=5) - Recent CI/CD status
187
+ 5. list_commits(repo_slug="${repoSlug}", limit=10) - Recent activity
188
+
189
+ Summarize:
190
+ - Repository description and purpose
191
+ - Current development activity
192
+ - Open pull requests needing attention
193
+ - CI/CD health
194
+ - Recent contributors`;
195
+
196
+ return {
197
+ messages: [
198
+ {
199
+ role: 'user',
200
+ content: {
201
+ type: 'text',
202
+ text: content,
203
+ },
204
+ },
205
+ ],
206
+ };
207
+ }
208
+
@@ -0,0 +1,160 @@
1
+ /**
2
+ * MCP Resources for Bitbucket Server
3
+ */
4
+
5
+ import { Resource } from '@modelcontextprotocol/sdk/types.js';
6
+ import { getClient } from './client.js';
7
+
8
+ /**
9
+ * Resource definitions for the MCP server
10
+ */
11
+ export const resourceDefinitions: Resource[] = [
12
+ {
13
+ uri: 'bitbucket://repositories',
14
+ name: 'Repositories',
15
+ description: 'List all repositories in the workspace',
16
+ mimeType: 'text/markdown',
17
+ },
18
+ {
19
+ uri: 'bitbucket://repositories/{repo_slug}',
20
+ name: 'Repository Details',
21
+ description: 'Get detailed information about a specific repository',
22
+ mimeType: 'text/markdown',
23
+ },
24
+ {
25
+ uri: 'bitbucket://repositories/{repo_slug}/branches',
26
+ name: 'Repository Branches',
27
+ description: 'List branches in a repository',
28
+ mimeType: 'text/markdown',
29
+ },
30
+ {
31
+ uri: 'bitbucket://repositories/{repo_slug}/pull-requests',
32
+ name: 'Pull Requests',
33
+ description: 'List open pull requests in a repository',
34
+ mimeType: 'text/markdown',
35
+ },
36
+ {
37
+ uri: 'bitbucket://projects',
38
+ name: 'Projects',
39
+ description: 'List all projects in the workspace',
40
+ mimeType: 'text/markdown',
41
+ },
42
+ ];
43
+
44
+ /**
45
+ * Handle resource read requests
46
+ */
47
+ export async function handleResourceRead(uri: string): Promise<string> {
48
+ const client = getClient();
49
+
50
+ // Parse the URI to extract parameters
51
+ if (uri === 'bitbucket://repositories') {
52
+ return await resourceRepositories(client);
53
+ }
54
+
55
+ if (uri === 'bitbucket://projects') {
56
+ return await resourceProjects(client);
57
+ }
58
+
59
+ // Match repository-specific URIs
60
+ const repoMatch = uri.match(/^bitbucket:\/\/repositories\/([^/]+)$/);
61
+ if (repoMatch) {
62
+ return await resourceRepository(client, repoMatch[1]);
63
+ }
64
+
65
+ const branchesMatch = uri.match(/^bitbucket:\/\/repositories\/([^/]+)\/branches$/);
66
+ if (branchesMatch) {
67
+ return await resourceBranches(client, branchesMatch[1]);
68
+ }
69
+
70
+ const prsMatch = uri.match(/^bitbucket:\/\/repositories\/([^/]+)\/pull-requests$/);
71
+ if (prsMatch) {
72
+ return await resourcePullRequests(client, prsMatch[1]);
73
+ }
74
+
75
+ throw new Error(`Unknown resource URI: ${uri}`);
76
+ }
77
+
78
+ async function resourceRepositories(client: ReturnType<typeof getClient>): Promise<string> {
79
+ const repos = await client.listRepositories({ limit: 50 });
80
+ const lines = [`# Repositories in ${client.workspace}`, ''];
81
+
82
+ for (const r of repos) {
83
+ const name = r.name || 'unknown';
84
+ const desc = (r.description || '').substring(0, 50) || 'No description';
85
+ const icon = r.is_private ? '🔒' : '🌐';
86
+ lines.push(`- ${icon} **${name}**: ${desc}`);
87
+ }
88
+
89
+ return lines.join('\n');
90
+ }
91
+
92
+ async function resourceRepository(client: ReturnType<typeof getClient>, repoSlug: string): Promise<string> {
93
+ const repo = await client.getRepository(repoSlug);
94
+ if (!repo) {
95
+ return `Repository '${repoSlug}' not found`;
96
+ }
97
+
98
+ const lines = [
99
+ `# ${repo.name || repoSlug}`,
100
+ '',
101
+ `**Description**: ${repo.description || 'No description'}`,
102
+ `**Private**: ${repo.is_private ? 'Yes' : 'No'}`,
103
+ `**Project**: ${repo.project?.name || 'None'}`,
104
+ `**Main branch**: ${repo.mainbranch?.name || 'main'}`,
105
+ '',
106
+ '## Clone URLs',
107
+ ];
108
+
109
+ for (const clone of repo.links?.clone || []) {
110
+ lines.push(`- ${clone.name}: \`${clone.href}\``);
111
+ }
112
+
113
+ return lines.join('\n');
114
+ }
115
+
116
+ async function resourceBranches(client: ReturnType<typeof getClient>, repoSlug: string): Promise<string> {
117
+ const branches = await client.listBranches(repoSlug, { limit: 30 });
118
+ const lines = [`# Branches in ${repoSlug}`, ''];
119
+
120
+ for (const b of branches) {
121
+ const name = b.name || 'unknown';
122
+ const commit = (b.target?.hash || '').substring(0, 7);
123
+ lines.push(`- **${name}** (${commit})`);
124
+ }
125
+
126
+ return lines.join('\n');
127
+ }
128
+
129
+ async function resourcePullRequests(client: ReturnType<typeof getClient>, repoSlug: string): Promise<string> {
130
+ const prs = await client.listPullRequests(repoSlug, { state: 'OPEN', limit: 20 });
131
+ const lines = [`# Open Pull Requests in ${repoSlug}`, ''];
132
+
133
+ if (prs.length === 0) {
134
+ lines.push('No open pull requests');
135
+ }
136
+
137
+ for (const pr of prs) {
138
+ const prId = pr.id;
139
+ const title = pr.title || 'Untitled';
140
+ const author = pr.author?.display_name || 'Unknown';
141
+ lines.push(`- **#${prId}**: ${title} (by ${author})`);
142
+ }
143
+
144
+ return lines.join('\n');
145
+ }
146
+
147
+ async function resourceProjects(client: ReturnType<typeof getClient>): Promise<string> {
148
+ const projects = await client.listProjects({ limit: 50 });
149
+ const lines = [`# Projects in ${client.workspace}`, ''];
150
+
151
+ for (const p of projects) {
152
+ const key = p.key || '?';
153
+ const name = p.name || 'Unknown';
154
+ const desc = (p.description || '').substring(0, 40) || 'No description';
155
+ lines.push(`- **${key}** - ${name}: ${desc}`);
156
+ }
157
+
158
+ return lines.join('\n');
159
+ }
160
+
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Settings management for Bitbucket MCP Server
3
+ *
4
+ * Configuration via environment variables:
5
+ * - BITBUCKET_WORKSPACE: Bitbucket workspace slug (required)
6
+ * - BITBUCKET_EMAIL: Account email for Basic Auth (required)
7
+ * - BITBUCKET_API_TOKEN: Repository access token (required)
8
+ * - API_TIMEOUT: Request timeout in seconds (default: 30, max: 300)
9
+ * - MAX_RETRIES: Max retry attempts for rate limiting (default: 3, max: 10)
10
+ * - OUTPUT_FORMAT: Output format - 'json' or 'toon' (default: json)
11
+ */
12
+
13
+ import { z } from 'zod';
14
+
15
+ const settingsSchema = z.object({
16
+ bitbucketWorkspace: z.string().min(1, 'BITBUCKET_WORKSPACE is required'),
17
+ bitbucketEmail: z.string().min(1, 'BITBUCKET_EMAIL is required'),
18
+ bitbucketApiToken: z.string().min(1, 'BITBUCKET_API_TOKEN is required'),
19
+ apiTimeout: z.number().min(1).max(300).default(30),
20
+ maxRetries: z.number().min(0).max(10).default(3),
21
+ outputFormat: z.enum(['json', 'toon']).default('json'),
22
+ });
23
+
24
+ export type Settings = z.infer<typeof settingsSchema>;
25
+
26
+ let cachedSettings: Settings | null = null;
27
+
28
+ /**
29
+ * Load and validate settings from environment variables.
30
+ * Results are cached for subsequent calls.
31
+ */
32
+ export function getSettings(): Settings {
33
+ if (cachedSettings) {
34
+ return cachedSettings;
35
+ }
36
+
37
+ const rawSettings = {
38
+ bitbucketWorkspace: process.env.BITBUCKET_WORKSPACE || '',
39
+ bitbucketEmail: process.env.BITBUCKET_EMAIL || '',
40
+ bitbucketApiToken: process.env.BITBUCKET_API_TOKEN || '',
41
+ apiTimeout: parseInt(process.env.API_TIMEOUT || '30', 10),
42
+ maxRetries: parseInt(process.env.MAX_RETRIES || '3', 10),
43
+ outputFormat: (process.env.OUTPUT_FORMAT || 'json') as 'json' | 'toon',
44
+ };
45
+
46
+ const result = settingsSchema.safeParse(rawSettings);
47
+
48
+ if (!result.success) {
49
+ const errors = result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
50
+ throw new Error(`Configuration error: ${errors}`);
51
+ }
52
+
53
+ cachedSettings = result.data;
54
+ return cachedSettings;
55
+ }
56
+
57
+ /**
58
+ * Reset cached settings (useful for testing)
59
+ */
60
+ export function resetSettings(): void {
61
+ cachedSettings = null;
62
+ }
63
+
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Branch tools for Bitbucket MCP Server
3
+ */
4
+
5
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
6
+ import { getClient } from '../client.js';
7
+ import { validateLimit, notFoundResponse } from '../utils.js';
8
+
9
+ export const definitions: Tool[] = [
10
+ {
11
+ name: 'list_branches',
12
+ description: 'List branches in a repository.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ repo_slug: { type: 'string', description: 'Repository slug' },
17
+ limit: { type: 'number', description: 'Maximum results (default: 50)', default: 50 },
18
+ },
19
+ required: ['repo_slug'],
20
+ },
21
+ },
22
+ {
23
+ name: 'get_branch',
24
+ description: 'Get information about a specific branch.',
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ repo_slug: { type: 'string', description: 'Repository slug' },
29
+ branch_name: { type: 'string', description: 'Branch name' },
30
+ },
31
+ required: ['repo_slug', 'branch_name'],
32
+ },
33
+ },
34
+ ];
35
+
36
+ export const handlers: Record<string, (args: Record<string, unknown>) => Promise<Record<string, unknown>>> = {
37
+ list_branches: async (args) => {
38
+ const client = getClient();
39
+ const branches = await client.listBranches(args.repo_slug as string, {
40
+ limit: validateLimit((args.limit as number) || 50),
41
+ });
42
+ return {
43
+ branches: branches.map(b => ({
44
+ name: b.name,
45
+ commit: b.target?.hash?.substring(0, 7),
46
+ message: b.target?.message,
47
+ date: b.target?.date,
48
+ })),
49
+ };
50
+ },
51
+
52
+ get_branch: async (args) => {
53
+ const client = getClient();
54
+ const result = await client.getBranch(args.repo_slug as string, args.branch_name as string);
55
+ if (!result) {
56
+ return notFoundResponse('Branch', args.branch_name as string);
57
+ }
58
+ return {
59
+ name: result.name,
60
+ latest_commit: {
61
+ hash: result.target?.hash,
62
+ message: result.target?.message || '',
63
+ author: result.target?.author?.raw,
64
+ date: result.target?.date,
65
+ },
66
+ };
67
+ },
68
+ };
69
+