bitbucket-mcp-server 1.0.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/LICENSE +15 -0
- package/README.md +236 -0
- package/build/bitbucket-api.d.ts +163 -0
- package/build/bitbucket-api.js +230 -0
- package/build/bitbucket-api.js.map +1 -0
- package/build/bitbucket-api.test.d.ts +1 -0
- package/build/bitbucket-api.test.js +203 -0
- package/build/bitbucket-api.test.js.map +1 -0
- package/build/cache.d.ts +150 -0
- package/build/cache.js +361 -0
- package/build/cache.js.map +1 -0
- package/build/config.d.ts +102 -0
- package/build/config.js +199 -0
- package/build/config.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +790 -0
- package/build/index.js.map +1 -0
- package/build/metrics.d.ts +81 -0
- package/build/metrics.js +199 -0
- package/build/metrics.js.map +1 -0
- package/build/pipelines.d.ts +1 -0
- package/build/pipelines.js +2 -0
- package/build/pipelines.js.map +1 -0
- package/build/rate-limiting.d.ts +53 -0
- package/build/rate-limiting.js +268 -0
- package/build/rate-limiting.js.map +1 -0
- package/build/validation.d.ts +7 -0
- package/build/validation.js +40 -0
- package/build/validation.js.map +1 -0
- package/build/validation.test.d.ts +1 -0
- package/build/validation.test.js +59 -0
- package/build/validation.test.js.map +1 -0
- package/build/webhooks.d.ts +168 -0
- package/build/webhooks.js +305 -0
- package/build/webhooks.js.map +1 -0
- package/package.json +48 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { BitbucketAPI } from "./bitbucket-api.js";
|
|
6
|
+
import { metricsCollector } from "./metrics.js";
|
|
7
|
+
import { configManager, validateEnvironment } from "./config.js";
|
|
8
|
+
import { MultiTierRateLimiter, createDefaultRateLimitConfig } from "./rate-limiting.js";
|
|
9
|
+
// Environment variables for authentication
|
|
10
|
+
const BITBUCKET_USERNAME = process.env.BITBUCKET_USERNAME;
|
|
11
|
+
const BITBUCKET_APP_PASSWORD = process.env.BITBUCKET_APP_PASSWORD;
|
|
12
|
+
// Validate environment on startup
|
|
13
|
+
const envValidation = validateEnvironment();
|
|
14
|
+
if (!envValidation.valid) {
|
|
15
|
+
console.error("Environment validation failed:");
|
|
16
|
+
envValidation.errors.forEach(error => console.error(` ❌ ${error}`));
|
|
17
|
+
if (envValidation.warnings.length > 0) {
|
|
18
|
+
console.error("Warnings:");
|
|
19
|
+
envValidation.warnings.forEach(warning => console.error(` ⚠️ ${warning}`));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Create rate limiter
|
|
23
|
+
const rateLimiter = new MultiTierRateLimiter(createDefaultRateLimitConfig());
|
|
24
|
+
// Create Bitbucket API instance
|
|
25
|
+
const bitbucketAPI = new BitbucketAPI();
|
|
26
|
+
// Create server instance
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: "bitbucket-mcp",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
capabilities: {
|
|
31
|
+
resources: {},
|
|
32
|
+
tools: {},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
// Tool: List repositories for a workspace
|
|
36
|
+
server.tool("list-repositories", "List repositories in a Bitbucket workspace", {
|
|
37
|
+
workspace: z.string().describe("Bitbucket workspace name (username or team name)"),
|
|
38
|
+
role: z.enum(["owner", "admin", "contributor", "member"]).optional().describe("Filter by user role"),
|
|
39
|
+
sort: z.enum(["created_on", "updated_on", "name", "size"]).optional().describe("Sort repositories by"),
|
|
40
|
+
}, async ({ workspace, role, sort }) => {
|
|
41
|
+
try {
|
|
42
|
+
const result = await bitbucketAPI.listRepositories(workspace);
|
|
43
|
+
const repositories = result.repositories;
|
|
44
|
+
if (repositories.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `No repositories found in workspace '${workspace}'.`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const repoText = repositories.map((repo) => [
|
|
55
|
+
`**${repo.name}** (${repo.full_name})`,
|
|
56
|
+
` Description: ${repo.description || "No description"}`,
|
|
57
|
+
` Language: ${repo.language || "Unknown"}`,
|
|
58
|
+
` Private: ${repo.is_private ? "Yes" : "No"}`,
|
|
59
|
+
` Size: ${repo.size} bytes`,
|
|
60
|
+
` Created: ${new Date(repo.created_on).toLocaleDateString()}`,
|
|
61
|
+
` Updated: ${new Date(repo.updated_on).toLocaleDateString()}`,
|
|
62
|
+
` Owner: ${repo.owner.display_name} (@${repo.owner.username})`,
|
|
63
|
+
` URL: ${repo.links.html.href}`,
|
|
64
|
+
"---",
|
|
65
|
+
].join("\n"));
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: `Found ${repositories.length} repositories in workspace '${workspace}':\n\n${repoText.join("\n")}`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: `Failed to retrieve repositories: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
// Tool: Get repository details
|
|
87
|
+
server.tool("get-repository", "Get detailed information about a specific repository", {
|
|
88
|
+
workspace: z.string().describe("Bitbucket workspace name"),
|
|
89
|
+
repo_slug: z.string().describe("Repository slug/name"),
|
|
90
|
+
}, async ({ workspace, repo_slug }) => {
|
|
91
|
+
try {
|
|
92
|
+
const repo = await bitbucketAPI.getRepository(workspace, repo_slug);
|
|
93
|
+
const cloneUrls = repo.links.clone?.map((link) => `${link.name}: ${link.href}`).join("\n ") || "No clone URLs available";
|
|
94
|
+
const repoInfo = [
|
|
95
|
+
`# ${repo.name}`,
|
|
96
|
+
`**Full Name:** ${repo.full_name}`,
|
|
97
|
+
`**Description:** ${repo.description || "No description"}`,
|
|
98
|
+
`**Language:** ${repo.language || "Unknown"}`,
|
|
99
|
+
`**Private:** ${repo.is_private ? "Yes" : "No"}`,
|
|
100
|
+
`**Size:** ${repo.size} bytes`,
|
|
101
|
+
`**Created:** ${new Date(repo.created_on).toLocaleString()}`,
|
|
102
|
+
`**Updated:** ${new Date(repo.updated_on).toLocaleString()}`,
|
|
103
|
+
`**Owner:** ${repo.owner.display_name} (@${repo.owner.username})`,
|
|
104
|
+
`**URL:** ${repo.links.html.href}`,
|
|
105
|
+
`**Clone URLs:**`,
|
|
106
|
+
` ${cloneUrls}`,
|
|
107
|
+
].join("\n");
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: repoInfo,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: `Failed to retrieve repository '${workspace}/${repo_slug}': ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// Tool: List pull requests
|
|
129
|
+
server.tool("list-pull-requests", "List pull requests for a repository", {
|
|
130
|
+
workspace: z.string().describe("Bitbucket workspace name"),
|
|
131
|
+
repo_slug: z.string().describe("Repository slug/name"),
|
|
132
|
+
state: z.enum(["OPEN", "MERGED", "DECLINED", "SUPERSEDED"]).optional().describe("Filter by PR state"),
|
|
133
|
+
}, async ({ workspace, repo_slug, state }) => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await bitbucketAPI.getPullRequests(workspace, repo_slug, state);
|
|
136
|
+
const pullRequests = result.pullRequests;
|
|
137
|
+
if (pullRequests.length === 0) {
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: `No pull requests found in '${workspace}/${repo_slug}'${state ? ` with state '${state}'` : ''}.`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const prText = pullRequests.map((pr) => [
|
|
148
|
+
`**PR #${pr.id}: ${pr.title}**`,
|
|
149
|
+
` State: ${pr.state}`,
|
|
150
|
+
` Author: ${pr.author.display_name} (@${pr.author.username})`,
|
|
151
|
+
` From: ${pr.source.repository.full_name}:${pr.source.branch.name}`,
|
|
152
|
+
` To: ${pr.destination.repository.full_name}:${pr.destination.branch.name}`,
|
|
153
|
+
` Created: ${new Date(pr.created_on).toLocaleDateString()}`,
|
|
154
|
+
` Updated: ${new Date(pr.updated_on).toLocaleDateString()}`,
|
|
155
|
+
` URL: ${pr.links.html.href}`,
|
|
156
|
+
` Description: ${pr.description || "No description"}`,
|
|
157
|
+
"---",
|
|
158
|
+
].join("\n"));
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: `Found ${pullRequests.length} pull requests in '${workspace}/${repo_slug}':\n\n${prText.join("\n")}`,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: "text",
|
|
173
|
+
text: `Failed to retrieve pull requests for '${workspace}/${repo_slug}': ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// Tool: Get pull request diff
|
|
180
|
+
server.tool("get-pr-diff", "Get the diff/changes for a specific pull request", {
|
|
181
|
+
workspace: z.string().describe("Bitbucket workspace name"),
|
|
182
|
+
repo_slug: z.string().describe("Repository slug/name"),
|
|
183
|
+
pull_request_id: z.number().describe("Pull request ID"),
|
|
184
|
+
}, async ({ workspace, repo_slug, pull_request_id }) => {
|
|
185
|
+
try {
|
|
186
|
+
const diff = await bitbucketAPI.getPullRequestDiff(workspace, repo_slug, pull_request_id);
|
|
187
|
+
if (!diff || diff.trim().length === 0) {
|
|
188
|
+
return {
|
|
189
|
+
content: [
|
|
190
|
+
{
|
|
191
|
+
type: "text",
|
|
192
|
+
text: `No changes found in pull request #${pull_request_id} for '${workspace}/${repo_slug}'. The PR might be empty or not exist.`,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Split diff into sections for better readability
|
|
198
|
+
const diffLines = diff.split('\n');
|
|
199
|
+
const fileChanges = [];
|
|
200
|
+
let currentFile = '';
|
|
201
|
+
let currentChanges = [];
|
|
202
|
+
for (const line of diffLines) {
|
|
203
|
+
if (line.startsWith('diff --git')) {
|
|
204
|
+
// Save previous file changes if any
|
|
205
|
+
if (currentFile && currentChanges.length > 0) {
|
|
206
|
+
fileChanges.push(`### ${currentFile}\n\`\`\`diff\n${currentChanges.join('\n')}\n\`\`\``);
|
|
207
|
+
}
|
|
208
|
+
// Start new file
|
|
209
|
+
currentFile = line.match(/b\/(.+)$/)?.[1] || 'Unknown file';
|
|
210
|
+
currentChanges = [line];
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
currentChanges.push(line);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Add the last file
|
|
217
|
+
if (currentFile && currentChanges.length > 0) {
|
|
218
|
+
fileChanges.push(`### ${currentFile}\n\`\`\`diff\n${currentChanges.join('\n')}\n\`\`\``);
|
|
219
|
+
}
|
|
220
|
+
// If no file-based parsing worked, show the raw diff
|
|
221
|
+
if (fileChanges.length === 0) {
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: "text",
|
|
226
|
+
text: [
|
|
227
|
+
`# 📝 Pull Request #${pull_request_id} Diff`,
|
|
228
|
+
`**Repository:** ${workspace}/${repo_slug}`,
|
|
229
|
+
"",
|
|
230
|
+
"```diff",
|
|
231
|
+
diff,
|
|
232
|
+
"```"
|
|
233
|
+
].join("\n"),
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: "text",
|
|
242
|
+
text: [
|
|
243
|
+
`# 📝 Pull Request #${pull_request_id} Diff`,
|
|
244
|
+
`**Repository:** ${workspace}/${repo_slug}`,
|
|
245
|
+
`**Files Changed:** ${fileChanges.length}`,
|
|
246
|
+
"",
|
|
247
|
+
...fileChanges
|
|
248
|
+
].join("\n"),
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
type: "text",
|
|
258
|
+
text: `Failed to retrieve diff for pull request #${pull_request_id} in '${workspace}/${repo_slug}': ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
// Tool: List issues
|
|
265
|
+
server.tool("list-issues", "List issues for a repository", {
|
|
266
|
+
workspace: z.string().describe("Bitbucket workspace name"),
|
|
267
|
+
repo_slug: z.string().describe("Repository slug/name"),
|
|
268
|
+
state: z.enum(["new", "open", "resolved", "on hold", "invalid", "duplicate", "wontfix", "closed"]).optional().describe("Filter by issue state"),
|
|
269
|
+
kind: z.enum(["bug", "enhancement", "proposal", "task"]).optional().describe("Filter by issue kind"),
|
|
270
|
+
}, async ({ workspace, repo_slug, state, kind }) => {
|
|
271
|
+
try {
|
|
272
|
+
const result = await bitbucketAPI.getIssues(workspace, repo_slug, state);
|
|
273
|
+
const issues = result.issues;
|
|
274
|
+
if (issues.length === 0) {
|
|
275
|
+
return {
|
|
276
|
+
content: [
|
|
277
|
+
{
|
|
278
|
+
type: "text",
|
|
279
|
+
text: `No issues found in '${workspace}/${repo_slug}'${state ? ` with state '${state}'` : ''}${kind ? ` and kind '${kind}'` : ''}.`,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const issueText = issues.map((issue) => [
|
|
285
|
+
`**Issue #${issue.id}: ${issue.title}**`,
|
|
286
|
+
` State: ${issue.state}`,
|
|
287
|
+
` Kind: ${issue.kind}`,
|
|
288
|
+
` Priority: ${issue.priority}`,
|
|
289
|
+
` Reporter: ${issue.reporter.display_name} (@${issue.reporter.username})`,
|
|
290
|
+
` Assignee: ${issue.assignee ? `${issue.assignee.display_name} (@${issue.assignee.username})` : "Unassigned"}`,
|
|
291
|
+
` Created: ${new Date(issue.created_on).toLocaleDateString()}`,
|
|
292
|
+
` Updated: ${new Date(issue.updated_on).toLocaleDateString()}`,
|
|
293
|
+
` URL: ${issue.links.html.href}`,
|
|
294
|
+
` Description: ${issue.content?.raw || "No description"}`,
|
|
295
|
+
"---",
|
|
296
|
+
].join("\n"));
|
|
297
|
+
return {
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: "text",
|
|
301
|
+
text: `Found ${issues.length} issues in '${workspace}/${repo_slug}':\n\n${issueText.join("\n")}`,
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
return {
|
|
308
|
+
content: [
|
|
309
|
+
{
|
|
310
|
+
type: "text",
|
|
311
|
+
text: `Failed to retrieve issues for '${workspace}/${repo_slug}': ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
// Tool: List branches
|
|
318
|
+
server.tool("list-branches", "List branches for a repository", {
|
|
319
|
+
workspace: z.string().describe("Bitbucket workspace name"),
|
|
320
|
+
repo_slug: z.string().describe("Repository slug/name"),
|
|
321
|
+
}, async ({ workspace, repo_slug }) => {
|
|
322
|
+
try {
|
|
323
|
+
const result = await bitbucketAPI.getBranches(workspace, repo_slug);
|
|
324
|
+
const branches = result.branches;
|
|
325
|
+
if (branches.length === 0) {
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: `No branches found in '${workspace}/${repo_slug}'.`,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const branchText = branches.map((branch) => [
|
|
336
|
+
`**${branch.name}**`,
|
|
337
|
+
` Last commit: ${branch.target.hash.substring(0, 8)}`,
|
|
338
|
+
` Commit message: ${branch.target.message}`,
|
|
339
|
+
` Author: ${branch.target.author.raw}`,
|
|
340
|
+
` Date: ${new Date(branch.target.date).toLocaleDateString()}`,
|
|
341
|
+
` URL: ${branch.links.html.href}`,
|
|
342
|
+
"---",
|
|
343
|
+
].join("\n"));
|
|
344
|
+
return {
|
|
345
|
+
content: [
|
|
346
|
+
{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: `Found ${branches.length} branches in '${workspace}/${repo_slug}':\n\n${branchText.join("\n")}`,
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
return {
|
|
355
|
+
content: [
|
|
356
|
+
{
|
|
357
|
+
type: "text",
|
|
358
|
+
text: `Failed to retrieve branches for '${workspace}/${repo_slug}': ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
// Tool: Get recent commits
|
|
365
|
+
server.tool("get-commits", "Get recent commits for a repository", {
|
|
366
|
+
workspace: z.string().describe("Bitbucket workspace name"),
|
|
367
|
+
repo_slug: z.string().describe("Repository slug/name"),
|
|
368
|
+
branch: z.string().optional().describe("Branch name (defaults to main branch)"),
|
|
369
|
+
limit: z.number().min(1).max(50).optional().default(10).describe("Number of commits to retrieve (1-50, default: 10)"),
|
|
370
|
+
}, async ({ workspace, repo_slug, branch, limit }) => {
|
|
371
|
+
try {
|
|
372
|
+
const result = await bitbucketAPI.getCommits(workspace, repo_slug, branch);
|
|
373
|
+
const commits = result.commits.slice(0, limit);
|
|
374
|
+
if (commits.length === 0) {
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: `No commits found in '${workspace}/${repo_slug}'${branch ? ` on branch '${branch}'` : ''}.`,
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const commitText = commits.map((commit) => [
|
|
385
|
+
`**${commit.hash.substring(0, 8)}** - ${commit.message.split('\n')[0]}`,
|
|
386
|
+
` Author: ${commit.author.user ? `${commit.author.user.display_name} (@${commit.author.user.username})` : commit.author.raw}`,
|
|
387
|
+
` Date: ${new Date(commit.date).toLocaleString()}`,
|
|
388
|
+
` URL: ${commit.links.html.href}`,
|
|
389
|
+
commit.message.includes('\n') ? ` Full message: ${commit.message}` : '',
|
|
390
|
+
"---",
|
|
391
|
+
].filter(line => line).join("\n"));
|
|
392
|
+
return {
|
|
393
|
+
content: [
|
|
394
|
+
{
|
|
395
|
+
type: "text",
|
|
396
|
+
text: `Found ${commits.length} recent commits in '${workspace}/${repo_slug}'${branch ? ` on branch '${branch}'` : ''}:\n\n${commitText.join("\n")}`,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
return {
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: "text",
|
|
406
|
+
text: `Failed to retrieve commits for '${workspace}/${repo_slug}': ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
// Tool: Health check - test API connectivity
|
|
413
|
+
server.tool("health-check", "Check connectivity to Bitbucket API and validate credentials", {
|
|
414
|
+
workspace: z.string().optional().describe("Optional workspace to test access"),
|
|
415
|
+
}, async ({ workspace }) => {
|
|
416
|
+
try {
|
|
417
|
+
const testWorkspace = workspace || "atlassian"; // Use Atlassian's public workspace as default
|
|
418
|
+
console.error(`Testing connectivity to Bitbucket API with workspace: ${testWorkspace}`);
|
|
419
|
+
const result = await bitbucketAPI.listRepositories(testWorkspace);
|
|
420
|
+
const authStatus = BITBUCKET_USERNAME && BITBUCKET_APP_PASSWORD ? "Authenticated" : "Unauthenticated (public access only)";
|
|
421
|
+
return {
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: "text",
|
|
425
|
+
text: [
|
|
426
|
+
"✅ **Bitbucket MCP Server Health Check**",
|
|
427
|
+
"",
|
|
428
|
+
`**API Status:** Connected successfully`,
|
|
429
|
+
`**Authentication:** ${authStatus}`,
|
|
430
|
+
`**Test Workspace:** ${testWorkspace}`,
|
|
431
|
+
`**Repositories Found:** ${result.repositories.length}`,
|
|
432
|
+
`**Has More Pages:** ${result.hasMore ? "Yes" : "No"}`,
|
|
433
|
+
"",
|
|
434
|
+
"**Available Tools:**",
|
|
435
|
+
"- list-repositories: ✅",
|
|
436
|
+
"- get-repository: ✅",
|
|
437
|
+
"- list-pull-requests: ✅",
|
|
438
|
+
"- get-pull-request: ✅",
|
|
439
|
+
"- list-issues: ✅",
|
|
440
|
+
"- list-branches: ✅",
|
|
441
|
+
"- get-commits: ✅",
|
|
442
|
+
"- health-check: ✅",
|
|
443
|
+
"",
|
|
444
|
+
"All systems operational! 🚀"
|
|
445
|
+
].join("\n"),
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
452
|
+
return {
|
|
453
|
+
content: [
|
|
454
|
+
{
|
|
455
|
+
type: "text",
|
|
456
|
+
text: [
|
|
457
|
+
"❌ **Bitbucket MCP Server Health Check Failed**",
|
|
458
|
+
"",
|
|
459
|
+
`**Error:** ${errorMessage}`,
|
|
460
|
+
`**Test Workspace:** ${workspace || "atlassian"}`,
|
|
461
|
+
"",
|
|
462
|
+
"**Possible Issues:**",
|
|
463
|
+
"- Network connectivity problems",
|
|
464
|
+
"- Invalid workspace name",
|
|
465
|
+
"- Authentication credentials issues (if using private repos)",
|
|
466
|
+
"- Bitbucket API service unavailable",
|
|
467
|
+
"",
|
|
468
|
+
"**Troubleshooting:**",
|
|
469
|
+
"1. Check your internet connection",
|
|
470
|
+
"2. Verify workspace name is correct",
|
|
471
|
+
"3. Ensure BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD are set for private repos",
|
|
472
|
+
"4. Check Bitbucket service status at https://status.atlassian.com/",
|
|
473
|
+
].join("\n"),
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
// Tool: Universal search across repositories, pull requests, issues, and commits
|
|
480
|
+
server.tool("search", "Search across repositories, pull requests, issues, and commits in a workspace", {
|
|
481
|
+
workspace: z.string().describe("Bitbucket workspace name"),
|
|
482
|
+
query: z.string().min(1).describe("Search query (searches in titles, descriptions, and content)"),
|
|
483
|
+
types: z.array(z.enum(["repositories", "pull-requests", "issues", "commits"])).optional().default(["repositories", "pull-requests", "issues"]).describe("Types of items to search"),
|
|
484
|
+
limit: z.number().min(1).max(50).optional().default(10).describe("Maximum number of results per type"),
|
|
485
|
+
}, async ({ workspace, query, types, limit }) => {
|
|
486
|
+
const searchResults = [];
|
|
487
|
+
let totalResults = 0;
|
|
488
|
+
try {
|
|
489
|
+
// Search repositories
|
|
490
|
+
if (types.includes("repositories")) {
|
|
491
|
+
try {
|
|
492
|
+
const repoResult = await bitbucketAPI.listRepositories(workspace);
|
|
493
|
+
const matchingRepos = repoResult.repositories.filter(repo => repo.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
494
|
+
(repo.description && repo.description.toLowerCase().includes(query.toLowerCase()))).slice(0, limit);
|
|
495
|
+
if (matchingRepos.length > 0) {
|
|
496
|
+
searchResults.push(`## 📁 Repositories (${matchingRepos.length} found)`);
|
|
497
|
+
matchingRepos.forEach(repo => {
|
|
498
|
+
searchResults.push([
|
|
499
|
+
`**${repo.name}** - ${repo.description || "No description"}`,
|
|
500
|
+
` Language: ${repo.language || "Unknown"} | Private: ${repo.is_private ? "Yes" : "No"}`,
|
|
501
|
+
` URL: ${repo.links.html.href}`,
|
|
502
|
+
""
|
|
503
|
+
].join("\n"));
|
|
504
|
+
});
|
|
505
|
+
totalResults += matchingRepos.length;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
searchResults.push(`## 📁 Repositories - Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// Search pull requests (sample from a few repositories)
|
|
513
|
+
if (types.includes("pull-requests")) {
|
|
514
|
+
try {
|
|
515
|
+
const repoResult = await bitbucketAPI.listRepositories(workspace);
|
|
516
|
+
const repos = repoResult.repositories.slice(0, 3); // Search in first 3 repos to avoid rate limits
|
|
517
|
+
for (const repo of repos) {
|
|
518
|
+
try {
|
|
519
|
+
const prResult = await bitbucketAPI.getPullRequests(workspace, repo.name);
|
|
520
|
+
const matchingPRs = prResult.pullRequests.filter(pr => pr.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
521
|
+
(pr.description && pr.description.toLowerCase().includes(query.toLowerCase()))).slice(0, Math.ceil(limit / repos.length));
|
|
522
|
+
if (matchingPRs.length > 0) {
|
|
523
|
+
if (totalResults === 0 || !searchResults.some(r => r.includes("Pull Requests"))) {
|
|
524
|
+
searchResults.push(`## 🔀 Pull Requests (${matchingPRs.length} found in ${repo.name})`);
|
|
525
|
+
}
|
|
526
|
+
matchingPRs.forEach(pr => {
|
|
527
|
+
searchResults.push([
|
|
528
|
+
`**PR #${pr.id}**: ${pr.title} (${repo.name})`,
|
|
529
|
+
` State: ${pr.state} | Author: ${pr.author.display_name}`,
|
|
530
|
+
` ${pr.source.branch.name} → ${pr.destination.branch.name}`,
|
|
531
|
+
` URL: ${pr.links.html.href}`,
|
|
532
|
+
""
|
|
533
|
+
].join("\n"));
|
|
534
|
+
});
|
|
535
|
+
totalResults += matchingPRs.length;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
// Silently continue if PR search fails for a repo
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
searchResults.push(`## 🔀 Pull Requests - Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Search issues (sample from a few repositories)
|
|
548
|
+
if (types.includes("issues")) {
|
|
549
|
+
try {
|
|
550
|
+
const repoResult = await bitbucketAPI.listRepositories(workspace);
|
|
551
|
+
const repos = repoResult.repositories.slice(0, 3); // Search in first 3 repos
|
|
552
|
+
for (const repo of repos) {
|
|
553
|
+
try {
|
|
554
|
+
const issueResult = await bitbucketAPI.getIssues(workspace, repo.name);
|
|
555
|
+
const matchingIssues = issueResult.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
556
|
+
(issue.content?.raw && issue.content.raw.toLowerCase().includes(query.toLowerCase()))).slice(0, Math.ceil(limit / repos.length));
|
|
557
|
+
if (matchingIssues.length > 0) {
|
|
558
|
+
if (totalResults === 0 || !searchResults.some(r => r.includes("Issues"))) {
|
|
559
|
+
searchResults.push(`## 🐛 Issues (${matchingIssues.length} found in ${repo.name})`);
|
|
560
|
+
}
|
|
561
|
+
matchingIssues.forEach(issue => {
|
|
562
|
+
searchResults.push([
|
|
563
|
+
`**Issue #${issue.id}**: ${issue.title} (${repo.name})`,
|
|
564
|
+
` State: ${issue.state} | Kind: ${issue.kind} | Priority: ${issue.priority}`,
|
|
565
|
+
` Reporter: ${issue.reporter.display_name}`,
|
|
566
|
+
` URL: ${issue.links.html.href}`,
|
|
567
|
+
""
|
|
568
|
+
].join("\n"));
|
|
569
|
+
});
|
|
570
|
+
totalResults += matchingIssues.length;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
// Silently continue if issue search fails for a repo
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
searchResults.push(`## 🐛 Issues - Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (totalResults === 0) {
|
|
583
|
+
return {
|
|
584
|
+
content: [
|
|
585
|
+
{
|
|
586
|
+
type: "text",
|
|
587
|
+
text: `No results found for query "${query}" in workspace "${workspace}". Try:\n- Different search terms\n- Checking if the workspace exists\n- Verifying you have access to the repositories`,
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
content: [
|
|
594
|
+
{
|
|
595
|
+
type: "text",
|
|
596
|
+
text: [
|
|
597
|
+
`# 🔍 Search Results for "${query}"`,
|
|
598
|
+
`**Workspace:** ${workspace}`,
|
|
599
|
+
`**Total Results:** ${totalResults}`,
|
|
600
|
+
"",
|
|
601
|
+
...searchResults
|
|
602
|
+
].join("\n"),
|
|
603
|
+
},
|
|
604
|
+
],
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
return {
|
|
609
|
+
content: [
|
|
610
|
+
{
|
|
611
|
+
type: "text",
|
|
612
|
+
text: `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
// Tool: Get performance metrics and insights
|
|
619
|
+
server.tool("get-metrics", "Get performance metrics, API usage statistics, and optimization insights", {
|
|
620
|
+
detailed: z.boolean().optional().describe("Include detailed metrics and recent request history"),
|
|
621
|
+
reset: z.boolean().optional().describe("Reset metrics after retrieving them"),
|
|
622
|
+
}, async ({ detailed = false, reset = false }) => {
|
|
623
|
+
try {
|
|
624
|
+
const metrics = detailed
|
|
625
|
+
? metricsCollector.getDetailedReport()
|
|
626
|
+
: { metrics: metricsCollector.getMetrics() };
|
|
627
|
+
const rateLimitStatus = rateLimiter.getStatus();
|
|
628
|
+
const configSummary = configManager.getSummary();
|
|
629
|
+
const insights = metricsCollector.getPerformanceInsights();
|
|
630
|
+
if (reset) {
|
|
631
|
+
metricsCollector.reset();
|
|
632
|
+
rateLimiter.reset();
|
|
633
|
+
}
|
|
634
|
+
const metricsText = [
|
|
635
|
+
"# 📊 Bitbucket MCP Server Metrics",
|
|
636
|
+
"",
|
|
637
|
+
"## 📈 Request Statistics",
|
|
638
|
+
`**Total Requests:** ${metrics.metrics.totalRequests}`,
|
|
639
|
+
`**Successful:** ${metrics.metrics.successfulRequests} (${metrics.metrics.totalRequests > 0 ? ((metrics.metrics.successfulRequests / metrics.metrics.totalRequests) * 100).toFixed(1) : 0}%)`,
|
|
640
|
+
`**Failed:** ${metrics.metrics.failedRequests}`,
|
|
641
|
+
`**Average Response Time:** ${metrics.metrics.averageResponseTime.toFixed(0)}ms`,
|
|
642
|
+
"",
|
|
643
|
+
"## 🔧 Tool Usage",
|
|
644
|
+
...Object.entries(metrics.metrics.requestsByTool).map(([tool, count]) => `**${tool}:** ${count} requests`),
|
|
645
|
+
"",
|
|
646
|
+
"## ⚡ Rate Limiting Status",
|
|
647
|
+
...Object.entries(rateLimitStatus).map(([tier, status]) => `**${tier.toUpperCase()}:** ${status.remaining || 0} requests remaining`),
|
|
648
|
+
"",
|
|
649
|
+
"## ⚙️ Configuration",
|
|
650
|
+
`**Authentication:** ${configManager.isAuthenticationConfigured() ? '✅ Configured' : '❌ Not configured'}`,
|
|
651
|
+
`**Base URL:** ${configSummary.baseUrl}`,
|
|
652
|
+
`**Timeout:** ${configSummary.timeout}ms`,
|
|
653
|
+
`**Retry Attempts:** ${configSummary.retryAttempts}`,
|
|
654
|
+
`**Metrics Enabled:** ${configSummary.enableMetrics ? '✅' : '❌'}`,
|
|
655
|
+
"",
|
|
656
|
+
"## 🎯 Performance Insights",
|
|
657
|
+
`**Overall Success Rate:** ${(insights.successRate * 100).toFixed(1)}%`,
|
|
658
|
+
""
|
|
659
|
+
];
|
|
660
|
+
if (insights.slowestEndpoints.length > 0) {
|
|
661
|
+
metricsText.push("### 🐌 Slowest Endpoints");
|
|
662
|
+
insights.slowestEndpoints.forEach(endpoint => {
|
|
663
|
+
metricsText.push(`**${endpoint.endpoint}:** ${endpoint.avgTime.toFixed(0)}ms average`);
|
|
664
|
+
});
|
|
665
|
+
metricsText.push("");
|
|
666
|
+
}
|
|
667
|
+
if (insights.mostUsedTools.length > 0) {
|
|
668
|
+
metricsText.push("### 🔥 Most Used Tools");
|
|
669
|
+
insights.mostUsedTools.forEach(tool => {
|
|
670
|
+
metricsText.push(`**${tool.tool}:** ${tool.count} requests`);
|
|
671
|
+
});
|
|
672
|
+
metricsText.push("");
|
|
673
|
+
}
|
|
674
|
+
if (insights.commonErrors.length > 0) {
|
|
675
|
+
metricsText.push("### ❌ Common Errors");
|
|
676
|
+
insights.commonErrors.forEach(error => {
|
|
677
|
+
metricsText.push(`**${error.error}:** ${error.count} occurrences`);
|
|
678
|
+
});
|
|
679
|
+
metricsText.push("");
|
|
680
|
+
}
|
|
681
|
+
if (insights.recommendedOptimizations.length > 0) {
|
|
682
|
+
metricsText.push("### 💡 Optimization Recommendations");
|
|
683
|
+
insights.recommendedOptimizations.forEach(rec => {
|
|
684
|
+
metricsText.push(`• ${rec}`);
|
|
685
|
+
});
|
|
686
|
+
metricsText.push("");
|
|
687
|
+
}
|
|
688
|
+
if (detailed && 'recentRequests' in metrics) {
|
|
689
|
+
metricsText.push("## 📋 Recent Requests");
|
|
690
|
+
if (metrics.recentRequests.length > 0) {
|
|
691
|
+
metrics.recentRequests.forEach(req => {
|
|
692
|
+
const status = req.success ? '✅' : '❌';
|
|
693
|
+
metricsText.push(`${status} **${req.tool}** (${req.endpoint}) - ${req.duration}ms`);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
metricsText.push("No recent requests");
|
|
698
|
+
}
|
|
699
|
+
metricsText.push("");
|
|
700
|
+
}
|
|
701
|
+
if (reset) {
|
|
702
|
+
metricsText.push("🔄 **Metrics have been reset**");
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
content: [
|
|
706
|
+
{
|
|
707
|
+
type: "text",
|
|
708
|
+
text: metricsText.join("\n"),
|
|
709
|
+
},
|
|
710
|
+
],
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
return {
|
|
715
|
+
content: [
|
|
716
|
+
{
|
|
717
|
+
type: "text",
|
|
718
|
+
text: `Failed to retrieve metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
// Tool: Get pull request details
|
|
725
|
+
server.tool("get-pull-request", "Get detailed information about a specific pull request", {
|
|
726
|
+
workspace: z.string().describe("Bitbucket workspace name"),
|
|
727
|
+
repo_slug: z.string().describe("Repository slug/name"),
|
|
728
|
+
pull_request_id: z.number().describe("Pull request ID"),
|
|
729
|
+
}, async ({ workspace, repo_slug, pull_request_id }) => {
|
|
730
|
+
try {
|
|
731
|
+
const pr = await bitbucketAPI.getPullRequest(workspace, repo_slug, pull_request_id);
|
|
732
|
+
const prInfo = [
|
|
733
|
+
`# 🔀 Pull Request #${pr.id}: ${pr.title}`,
|
|
734
|
+
`**Repository:** ${workspace}/${repo_slug}`,
|
|
735
|
+
`**State:** ${pr.state}`,
|
|
736
|
+
`**Author:** ${pr.author.display_name} (@${pr.author.username})`,
|
|
737
|
+
`**Created:** ${new Date(pr.created_on).toLocaleString()}`,
|
|
738
|
+
`**Updated:** ${new Date(pr.updated_on).toLocaleString()}`,
|
|
739
|
+
`**Source:** ${pr.source.repository.full_name}:${pr.source.branch.name}`,
|
|
740
|
+
`**Destination:** ${pr.destination.repository.full_name}:${pr.destination.branch.name}`,
|
|
741
|
+
`**URL:** ${pr.links.html.href}`,
|
|
742
|
+
"",
|
|
743
|
+
"## Description",
|
|
744
|
+
pr.description || "_No description provided_",
|
|
745
|
+
].join("\n");
|
|
746
|
+
return {
|
|
747
|
+
content: [
|
|
748
|
+
{
|
|
749
|
+
type: "text",
|
|
750
|
+
text: prInfo,
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
return {
|
|
757
|
+
content: [
|
|
758
|
+
{
|
|
759
|
+
type: "text",
|
|
760
|
+
text: `Failed to retrieve pull request #${pull_request_id} from '${workspace}/${repo_slug}': ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
761
|
+
},
|
|
762
|
+
],
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
// Main function to run the server
|
|
767
|
+
async function main() {
|
|
768
|
+
const transport = new StdioServerTransport();
|
|
769
|
+
await server.connect(transport);
|
|
770
|
+
// Log startup message to stderr so it doesn't interfere with MCP communication
|
|
771
|
+
console.error("🚀 Bitbucket MCP Server v1.0.0 running on stdio");
|
|
772
|
+
console.error("📋 Available tools: list-repositories, get-repository, list-pull-requests, get-pull-request, list-issues, list-branches, get-commits, health-check, search, get-metrics, get-pr-diff");
|
|
773
|
+
console.error(`⚙️ Configuration: ${configManager.isAuthenticationConfigured() ? '✅ Authenticated' : '❌ No authentication'}`);
|
|
774
|
+
console.error(`📊 Metrics: ${configManager.get('enableMetrics') ? '✅ Enabled' : '❌ Disabled'}`);
|
|
775
|
+
if (!BITBUCKET_USERNAME || !BITBUCKET_APP_PASSWORD) {
|
|
776
|
+
console.error("⚠️ WARNING: BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD environment variables not set.");
|
|
777
|
+
console.error(" Some functionality may be limited to public repositories only.");
|
|
778
|
+
}
|
|
779
|
+
// Log configuration validation
|
|
780
|
+
const configValidation = configManager.validate();
|
|
781
|
+
if (!configValidation.valid) {
|
|
782
|
+
console.error("❌ Configuration issues detected:");
|
|
783
|
+
configValidation.errors.forEach(error => console.error(` • ${error}`));
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
main().catch((error) => {
|
|
787
|
+
console.error("Fatal error in main():", error);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
});
|
|
790
|
+
//# sourceMappingURL=index.js.map
|