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/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