@tugudush/bitbucket-mcp 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/tools.js ADDED
@@ -0,0 +1,710 @@
1
+ import { zodToJsonSchema } from 'zod-to-json-schema';
2
+ import { BitbucketApiError } from './errors.js';
3
+ import { GetRepositorySchema, ListRepositoriesSchema, ListWorkspacesSchema, GetPullRequestsSchema, GetPullRequestSchema, GetPullRequestCommentsSchema, GetPullRequestActivitySchema, GetIssuesSchema, GetIssueSchema, GetCommitsSchema, GetBranchesSchema, GetFileContentSchema, BrowseRepositorySchema, GetUserSchema, GetCurrentUserSchema, SearchRepositoriesSchema, SearchCodeSchema, GetWorkspaceSchema, API_CONSTANTS, } from './schemas.js';
4
+ import { makeRequest, buildApiUrl, addQueryParams } from './api.js';
5
+ /**
6
+ * Tool definitions for the Bitbucket MCP server
7
+ */
8
+ export function getToolDefinitions() {
9
+ return [
10
+ {
11
+ name: 'bb_get_repository',
12
+ description: 'Get detailed information about a specific repository',
13
+ inputSchema: zodToJsonSchema(GetRepositorySchema),
14
+ },
15
+ {
16
+ name: 'bb_list_repositories',
17
+ description: 'List repositories in a workspace',
18
+ inputSchema: zodToJsonSchema(ListRepositoriesSchema),
19
+ },
20
+ {
21
+ name: 'bb_list_workspaces',
22
+ description: 'List all accessible workspaces for discovery and exploration',
23
+ inputSchema: zodToJsonSchema(ListWorkspacesSchema),
24
+ },
25
+ {
26
+ name: 'bb_get_pull_requests',
27
+ description: 'Get pull requests for a repository',
28
+ inputSchema: zodToJsonSchema(GetPullRequestsSchema),
29
+ },
30
+ {
31
+ name: 'bb_get_pull_request',
32
+ description: 'Get detailed information about a specific pull request',
33
+ inputSchema: zodToJsonSchema(GetPullRequestSchema),
34
+ },
35
+ {
36
+ name: 'bb_get_pull_request_comments',
37
+ description: 'Get comments for a specific pull request',
38
+ inputSchema: zodToJsonSchema(GetPullRequestCommentsSchema),
39
+ },
40
+ {
41
+ name: 'bb_get_pull_request_activity',
42
+ description: 'Get activity (reviews, approvals, comments) for a specific pull request',
43
+ inputSchema: zodToJsonSchema(GetPullRequestActivitySchema),
44
+ },
45
+ {
46
+ name: 'bb_get_issues',
47
+ description: 'Get issues for a repository',
48
+ inputSchema: zodToJsonSchema(GetIssuesSchema),
49
+ },
50
+ {
51
+ name: 'bb_get_issue',
52
+ description: 'Get detailed information about a specific issue',
53
+ inputSchema: zodToJsonSchema(GetIssueSchema),
54
+ },
55
+ {
56
+ name: 'bb_get_commits',
57
+ description: 'Get commits for a repository branch',
58
+ inputSchema: zodToJsonSchema(GetCommitsSchema),
59
+ },
60
+ {
61
+ name: 'bb_get_branches',
62
+ description: 'Get branches for a repository',
63
+ inputSchema: zodToJsonSchema(GetBranchesSchema),
64
+ },
65
+ {
66
+ name: 'bb_get_file_content',
67
+ description: 'Get the content of a file from a repository with pagination support',
68
+ inputSchema: zodToJsonSchema(GetFileContentSchema),
69
+ },
70
+ {
71
+ name: 'bb_browse_repository',
72
+ description: 'Browse files and directories in a repository to explore structure',
73
+ inputSchema: zodToJsonSchema(BrowseRepositorySchema),
74
+ },
75
+ {
76
+ name: 'bb_get_user',
77
+ description: 'Get information about a Bitbucket user. If no username is provided, returns information about the authenticated user.',
78
+ inputSchema: zodToJsonSchema(GetUserSchema),
79
+ },
80
+ {
81
+ name: 'bb_get_current_user',
82
+ description: 'Get information about the currently authenticated user.',
83
+ inputSchema: zodToJsonSchema(GetCurrentUserSchema),
84
+ },
85
+ {
86
+ name: 'bb_search_repositories',
87
+ description: 'Search for repositories within a workspace by name or description.',
88
+ inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
89
+ },
90
+ {
91
+ name: 'bb_search_code',
92
+ description: 'Search for code content within a workspace. Supports filtering by repository, language, and file extension.',
93
+ inputSchema: zodToJsonSchema(SearchCodeSchema),
94
+ },
95
+ {
96
+ name: 'bb_get_workspace',
97
+ description: 'Get information about a workspace',
98
+ inputSchema: zodToJsonSchema(GetWorkspaceSchema),
99
+ },
100
+ ];
101
+ }
102
+ export async function handleToolCall(request) {
103
+ const { name, arguments: args } = request.params;
104
+ try {
105
+ switch (name) {
106
+ case 'bb_get_repository': {
107
+ const parsed = GetRepositorySchema.parse(args);
108
+ const url = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}`);
109
+ const data = await makeRequest(url);
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: `Repository: ${data.full_name}\n` +
115
+ `Description: ${data.description || 'No description'}\n` +
116
+ `Language: ${data.language || 'Not specified'}\n` +
117
+ `Private: ${data.is_private}\n` +
118
+ `Created: ${data.created_on}\n` +
119
+ `Updated: ${data.updated_on}\n` +
120
+ `Size: ${data.size ? `${data.size} bytes` : 'Unknown'}\n` +
121
+ `Forks: ${data.forks_count || 0}\n` +
122
+ `Watchers: ${data.watchers_count || 0}\n` +
123
+ `Website: ${data.website || 'None'}`,
124
+ },
125
+ ],
126
+ };
127
+ }
128
+ case 'bb_list_repositories': {
129
+ const parsed = ListRepositoriesSchema.parse(args);
130
+ const params = {
131
+ page: parsed.page,
132
+ pagelen: parsed.pagelen,
133
+ };
134
+ const url = addQueryParams(buildApiUrl(`/repositories/${parsed.workspace}`), params);
135
+ const data = await makeRequest(url);
136
+ const repoList = data.values
137
+ .map((repo) => `- ${repo.full_name} (${repo.language || 'Unknown'})\n` +
138
+ ` ${repo.description || 'No description'}\n` +
139
+ ` Private: ${repo.is_private}, Updated: ${repo.updated_on}`)
140
+ .join('\n\n');
141
+ return {
142
+ content: [
143
+ {
144
+ type: 'text',
145
+ text: `Repositories in ${parsed.workspace} (${data.size} total):\n\n${repoList}`,
146
+ },
147
+ ],
148
+ };
149
+ }
150
+ case 'bb_list_workspaces': {
151
+ const parsed = ListWorkspacesSchema.parse(args);
152
+ const params = {
153
+ page: parsed.page,
154
+ pagelen: parsed.pagelen,
155
+ };
156
+ const url = addQueryParams(buildApiUrl('/workspaces'), params);
157
+ const data = await makeRequest(url);
158
+ const workspaceList = data.values
159
+ .map((workspace) => `- ${workspace.slug} (${workspace.name})\n` +
160
+ ` Type: ${workspace.type}\n` +
161
+ ` Created: ${workspace.created_on || 'Unknown'}`)
162
+ .join('\n\n');
163
+ return {
164
+ content: [
165
+ {
166
+ type: 'text',
167
+ text: `Accessible workspaces (${data.size} total):\n\n${workspaceList}`,
168
+ },
169
+ ],
170
+ };
171
+ }
172
+ case 'bb_get_pull_requests': {
173
+ const parsed = GetPullRequestsSchema.parse(args);
174
+ const params = {
175
+ state: parsed.state,
176
+ page: parsed.page,
177
+ pagelen: parsed.pagelen,
178
+ };
179
+ const url = addQueryParams(buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/pullrequests`), params);
180
+ const data = await makeRequest(url);
181
+ const prList = data.values
182
+ .map((pr) => `- #${pr.id}: ${pr.title}\n` +
183
+ ` Author: ${pr.author.display_name}\n` +
184
+ ` State: ${pr.state}\n` +
185
+ ` Created: ${pr.created_on}\n` +
186
+ ` Source: ${pr.source.branch.name} → ${pr.destination.branch.name}`)
187
+ .join('\n\n');
188
+ return {
189
+ content: [
190
+ {
191
+ type: 'text',
192
+ text: `Pull requests for ${parsed.workspace}/${parsed.repo_slug} (${data.size} total):\n\n${prList}`,
193
+ },
194
+ ],
195
+ };
196
+ }
197
+ case 'bb_get_pull_request': {
198
+ const parsed = GetPullRequestSchema.parse(args);
199
+ const url = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/pullrequests/${parsed.pull_request_id}`);
200
+ const data = await makeRequest(url);
201
+ return {
202
+ content: [
203
+ {
204
+ type: 'text',
205
+ text: `Pull Request #${data.id}: ${data.title}\n` +
206
+ `Author: ${data.author.display_name}\n` +
207
+ `State: ${data.state}\n` +
208
+ `Created: ${data.created_on}\n` +
209
+ `Updated: ${data.updated_on}\n` +
210
+ `Source: ${data.source.branch.name} → ${data.destination.branch.name}\n` +
211
+ `Description:\n${data.description || 'No description'}\n` +
212
+ `Reviewers: ${data.reviewers?.map(r => r.display_name).join(', ') || 'None'}`,
213
+ },
214
+ ],
215
+ };
216
+ }
217
+ case 'bb_get_pull_request_comments': {
218
+ const parsed = GetPullRequestCommentsSchema.parse(args);
219
+ const params = {
220
+ page: parsed.page,
221
+ pagelen: parsed.pagelen,
222
+ };
223
+ const url = addQueryParams(buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/pullrequests/${parsed.pull_request_id}/comments`), params);
224
+ const data = await makeRequest(url);
225
+ const commentList = data.values
226
+ .map((comment) => `- ${comment.user.display_name} (${comment.created_on}):\n` +
227
+ ` ${comment.content?.raw || 'No content'}\n` +
228
+ (comment.inline
229
+ ? ` File: ${comment.inline.path}, Line: ${comment.inline.to || comment.inline.from}`
230
+ : ''))
231
+ .join('\n\n');
232
+ return {
233
+ content: [
234
+ {
235
+ type: 'text',
236
+ text: `Comments for PR #${parsed.pull_request_id} (${data.size} total):\n\n${commentList}`,
237
+ },
238
+ ],
239
+ };
240
+ }
241
+ case 'bb_get_pull_request_activity': {
242
+ const parsed = GetPullRequestActivitySchema.parse(args);
243
+ const params = {
244
+ page: parsed.page,
245
+ pagelen: parsed.pagelen,
246
+ };
247
+ const url = addQueryParams(buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/pullrequests/${parsed.pull_request_id}/activity`), params);
248
+ const data = await makeRequest(url);
249
+ const activityList = data.values
250
+ .map((activity) => {
251
+ // Handle cases where user might be undefined/null (system activities)
252
+ const userName = activity.user?.display_name || 'System';
253
+ const actionDate = activity.created_on || activity.update?.date || 'Unknown date';
254
+ const action = activity.action || 'Activity';
255
+ let activityText = `- ${userName} (${actionDate}):\n` + ` Action: ${action}`;
256
+ // Add comment if present
257
+ if (activity.comment) {
258
+ activityText += `\n Comment: ${activity.comment.content?.raw || 'No content'}`;
259
+ }
260
+ // Add approval if present
261
+ if (activity.approval) {
262
+ activityText += `\n Approval: ${activity.approval.state || 'Unknown state'}`;
263
+ }
264
+ // Add update if present
265
+ if (activity.update) {
266
+ const updateAuthor = activity.update.author?.display_name || 'Unknown';
267
+ activityText += `\n Update: ${activity.update.state || 'Updated'} by ${updateAuthor}`;
268
+ if (activity.update.title) {
269
+ activityText += `\n Title changed to: ${activity.update.title}`;
270
+ }
271
+ }
272
+ return activityText;
273
+ })
274
+ .join('\n\n');
275
+ return {
276
+ content: [
277
+ {
278
+ type: 'text',
279
+ text: `Activity for PR #${parsed.pull_request_id} (${data.size} total):\n\n${activityList}`,
280
+ },
281
+ ],
282
+ };
283
+ }
284
+ case 'bb_get_issues': {
285
+ const parsed = GetIssuesSchema.parse(args);
286
+ const params = {
287
+ state: parsed.state,
288
+ kind: parsed.kind,
289
+ page: parsed.page,
290
+ pagelen: parsed.pagelen,
291
+ };
292
+ const url = addQueryParams(buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/issues`), params);
293
+ const data = await makeRequest(url);
294
+ const issueList = data.values
295
+ .map((issue) => `- #${issue.id}: ${issue.title}\n` +
296
+ ` State: ${issue.state}\n` +
297
+ ` Kind: ${issue.kind}\n` +
298
+ ` Priority: ${issue.priority}\n` +
299
+ ` Reporter: ${issue.reporter.display_name}\n` +
300
+ ` Assignee: ${issue.assignee?.display_name || 'Unassigned'}\n` +
301
+ ` Created: ${issue.created_on}`)
302
+ .join('\n\n');
303
+ return {
304
+ content: [
305
+ {
306
+ type: 'text',
307
+ text: `Issues for ${parsed.workspace}/${parsed.repo_slug} (${data.size} total):\n\n${issueList}`,
308
+ },
309
+ ],
310
+ };
311
+ }
312
+ case 'bb_get_issue': {
313
+ const parsed = GetIssueSchema.parse(args);
314
+ const url = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/issues/${parsed.issue_id}`);
315
+ const data = await makeRequest(url);
316
+ return {
317
+ content: [
318
+ {
319
+ type: 'text',
320
+ text: `Issue #${data.id}: ${data.title}\n` +
321
+ `State: ${data.state}\n` +
322
+ `Kind: ${data.kind}\n` +
323
+ `Priority: ${data.priority}\n` +
324
+ `Reporter: ${data.reporter.display_name}\n` +
325
+ `Assignee: ${data.assignee?.display_name || 'Unassigned'}\n` +
326
+ `Created: ${data.created_on}\n` +
327
+ `Updated: ${data.updated_on}\n` +
328
+ `Content:\n${data.content?.raw || 'No content'}`,
329
+ },
330
+ ],
331
+ };
332
+ }
333
+ case 'bb_get_commits': {
334
+ const parsed = GetCommitsSchema.parse(args);
335
+ const params = {
336
+ page: parsed.page,
337
+ pagelen: parsed.pagelen,
338
+ };
339
+ let url = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/commits`);
340
+ if (parsed.branch) {
341
+ url += `/${parsed.branch}`;
342
+ }
343
+ url = addQueryParams(url, params);
344
+ const data = await makeRequest(url);
345
+ const commitList = data.values
346
+ .map((commit) => `- ${commit.hash.substring(0, 8)}: ${commit.message.split('\n')[0]}\n` +
347
+ ` Author: ${commit.author.user?.display_name || commit.author.raw}\n` +
348
+ ` Date: ${commit.date}`)
349
+ .join('\n\n');
350
+ return {
351
+ content: [
352
+ {
353
+ type: 'text',
354
+ text: `Commits for ${parsed.workspace}/${parsed.repo_slug}${parsed.branch ? ` (${parsed.branch})` : ''} (${data.size} total):\n\n${commitList}`,
355
+ },
356
+ ],
357
+ };
358
+ }
359
+ case 'bb_get_branches': {
360
+ const parsed = GetBranchesSchema.parse(args);
361
+ const params = {
362
+ page: parsed.page,
363
+ pagelen: parsed.pagelen,
364
+ };
365
+ const url = addQueryParams(buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/refs/branches`), params);
366
+ const data = await makeRequest(url);
367
+ const branchList = data.values
368
+ .map((branch) => `- ${branch.name}\n` +
369
+ ` Last commit: ${branch.target.hash.substring(0, 8)}\n` +
370
+ ` Date: ${branch.target.date}`)
371
+ .join('\n\n');
372
+ return {
373
+ content: [
374
+ {
375
+ type: 'text',
376
+ text: `Branches for ${parsed.workspace}/${parsed.repo_slug} (${data.size} total):\n\n${branchList}`,
377
+ },
378
+ ],
379
+ };
380
+ }
381
+ case 'bb_get_file_content': {
382
+ const parsed = GetFileContentSchema.parse(args);
383
+ let ref = parsed.ref || 'HEAD';
384
+ // If no ref specified, fetch repository info to get default branch
385
+ if (ref === 'HEAD') {
386
+ try {
387
+ const repoUrl = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}`);
388
+ const repoData = await makeRequest(repoUrl);
389
+ ref = repoData.mainbranch?.name || 'main';
390
+ }
391
+ catch {
392
+ // Fallback to 'main' if we can't get repository info
393
+ ref = 'main';
394
+ }
395
+ }
396
+ // Use the same robust approach as bb_browse_repository
397
+ // Get commit SHA first to handle branch names with slashes
398
+ let url;
399
+ try {
400
+ const branchUrl = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/refs/branches/${encodeURIComponent(ref)}`);
401
+ const branchData = await makeRequest(branchUrl);
402
+ const commitSha = branchData.target.hash;
403
+ // Use /src/{commit_sha}/{file_path} pattern
404
+ const encodedFilePath = parsed.file_path
405
+ .split('/')
406
+ .map(segment => encodeURIComponent(segment))
407
+ .join('/');
408
+ url = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/src/${commitSha}/${encodedFilePath}`);
409
+ }
410
+ catch {
411
+ // If we can't get the commit SHA, fall back to trying the branch name directly
412
+ const encodedRef = encodeURIComponent(ref);
413
+ const encodedFilePath = parsed.file_path
414
+ .split('/')
415
+ .map(segment => encodeURIComponent(segment))
416
+ .join('/');
417
+ url = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/src/${encodedRef}/${encodedFilePath}`);
418
+ }
419
+ // Use a custom request for text content instead of makeRequest which expects JSON
420
+ const { loadConfig } = await import('./config.js');
421
+ const config = loadConfig();
422
+ const headers = {
423
+ Accept: 'text/plain',
424
+ 'User-Agent': 'bitbucket-mcp-server/1.0.0',
425
+ };
426
+ // Add authentication if available
427
+ const apiToken = config.BITBUCKET_API_TOKEN;
428
+ const email = config.BITBUCKET_EMAIL;
429
+ if (apiToken && email) {
430
+ const auth = Buffer.from(`${email}:${apiToken}`).toString('base64');
431
+ headers.Authorization = `Basic ${auth}`;
432
+ }
433
+ const response = await fetch(url, { headers });
434
+ if (!response.ok) {
435
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
436
+ }
437
+ const content = await response.text();
438
+ // Handle pagination
439
+ const lines = content.split('\n');
440
+ const start = parsed.start ? Math.max(1, parsed.start) : 1;
441
+ const limit = parsed.limit
442
+ ? Math.min(parsed.limit, API_CONSTANTS.MAX_FILE_LINES)
443
+ : API_CONSTANTS.DEFAULT_FILE_LINES;
444
+ const endLine = Math.min(start + limit - 1, lines.length);
445
+ const paginatedLines = lines.slice(start - 1, endLine);
446
+ return {
447
+ content: [
448
+ {
449
+ type: 'text',
450
+ text: `File: ${parsed.file_path} (lines ${start}-${endLine} of ${lines.length})\n` +
451
+ `Repository: ${parsed.workspace}/${parsed.repo_slug}\n` +
452
+ `Ref: ${ref}\n\n` +
453
+ paginatedLines
454
+ .map((line, index) => `${start + index}: ${line}`)
455
+ .join('\n'),
456
+ },
457
+ ],
458
+ };
459
+ }
460
+ case 'bb_browse_repository': {
461
+ const parsed = BrowseRepositorySchema.parse(args);
462
+ let ref = parsed.ref;
463
+ // If no ref specified, fetch repository info to get default branch
464
+ if (!ref) {
465
+ try {
466
+ const repoUrl = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}`);
467
+ const repoData = await makeRequest(repoUrl);
468
+ ref = repoData.mainbranch?.name || 'main';
469
+ }
470
+ catch {
471
+ // Fallback to 'main' if we can't get repository info
472
+ ref = 'main';
473
+ }
474
+ }
475
+ const path = parsed.path || '';
476
+ let url;
477
+ if (path) {
478
+ // For subdirectories, we need to get the commit SHA first
479
+ // because the /src/{ref}/{path} pattern doesn't work with branch names containing slashes
480
+ try {
481
+ const branchUrl = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/refs/branches/${encodeURIComponent(ref)}`);
482
+ const branchData = await makeRequest(branchUrl);
483
+ const commitSha = branchData.target.hash;
484
+ // Use /src/{commit_sha}/{path} pattern for subdirectories
485
+ const encodedPath = path
486
+ .split('/')
487
+ .map(segment => encodeURIComponent(segment))
488
+ .join('/');
489
+ url = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/src/${commitSha}/${encodedPath}`);
490
+ // Ensure trailing slash for directory browsing
491
+ if (!url.endsWith('/')) {
492
+ url += '/';
493
+ }
494
+ }
495
+ catch {
496
+ // If we can't get the commit SHA, fall back to trying the branch name directly
497
+ const encodedRef = encodeURIComponent(ref);
498
+ const encodedPath = path
499
+ .split('/')
500
+ .map(segment => encodeURIComponent(segment))
501
+ .join('/');
502
+ url = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/src/${encodedRef}/${encodedPath}`);
503
+ // Ensure trailing slash for directory browsing
504
+ if (!url.endsWith('/')) {
505
+ url += '/';
506
+ }
507
+ }
508
+ }
509
+ else {
510
+ // For root directory, use /src?at={ref} pattern (works with branch names)
511
+ url = buildApiUrl(`/repositories/${parsed.workspace}/${parsed.repo_slug}/src`);
512
+ // Ensure trailing slash for directory browsing
513
+ if (!url.endsWith('/')) {
514
+ url += '/';
515
+ }
516
+ url += `?at=${encodeURIComponent(ref)}`;
517
+ }
518
+ try {
519
+ const data = await makeRequest(url);
520
+ const limit = parsed.limit
521
+ ? Math.min(parsed.limit, API_CONSTANTS.MAX_BROWSE_ITEMS)
522
+ : API_CONSTANTS.DEFAULT_BROWSE_ITEMS;
523
+ const items = data.values.slice(0, limit);
524
+ const itemList = items
525
+ .map(item => {
526
+ const isDir = item.type === 'commit_directory';
527
+ const icon = isDir ? '📁' : '📄';
528
+ const size = item.size ? ` (${item.size} bytes)` : '';
529
+ return `${icon} ${item.path}${size}`;
530
+ })
531
+ .join('\n');
532
+ return {
533
+ content: [
534
+ {
535
+ type: 'text',
536
+ text: `Repository: ${parsed.workspace}/${parsed.repo_slug}\n` +
537
+ `Path: /${path}\n` +
538
+ `Ref: ${ref}\n` +
539
+ `Items (${items.length} of ${data.size || data.values.length} total):\n\n${itemList}`,
540
+ },
541
+ ],
542
+ };
543
+ }
544
+ catch (error) {
545
+ if (error instanceof BitbucketApiError && error.status === 404) {
546
+ // Enhanced error message for branch/commit not found
547
+ throw new BitbucketApiError(404, 'Not Found', `Branch, tag, or commit '${ref}' not found in repository ${parsed.workspace}/${parsed.repo_slug}`, `Try specifying a different branch with the 'ref' parameter. Common branch names are 'main', 'master', or 'develop'. Use bb_get_branches to list available branches.`);
548
+ }
549
+ throw error;
550
+ }
551
+ }
552
+ case 'bb_get_user': {
553
+ const parsed = GetUserSchema.parse(args);
554
+ // Bitbucket API v2.0 only supports getting current user info
555
+ // The /users/{username} endpoint doesn't exist
556
+ if (parsed.username) {
557
+ throw new Error(`Getting user info by username is not supported by Bitbucket API v2.0. Use bb_get_current_user for current user or bb_get_workspace for workspace info.`);
558
+ }
559
+ const url = buildApiUrl('/user');
560
+ const data = await makeRequest(url);
561
+ return {
562
+ content: [
563
+ {
564
+ type: 'text',
565
+ text: `User: ${data.display_name} (@${data.username})\n` +
566
+ `Account ID: ${data.account_id}\n` +
567
+ `Type: ${data.type}\n` +
568
+ `Website: ${data.website || 'None'}\n` +
569
+ `Location: ${data.location || 'Not specified'}\n` +
570
+ `Created: ${data.created_on}`,
571
+ },
572
+ ],
573
+ };
574
+ }
575
+ case 'bb_get_current_user': {
576
+ const url = buildApiUrl('/user');
577
+ const data = await makeRequest(url);
578
+ return {
579
+ content: [
580
+ {
581
+ type: 'text',
582
+ text: `Current User: ${data.display_name} (@${data.username})\n` +
583
+ `Account ID: ${data.account_id}\n` +
584
+ `Type: ${data.type}\n` +
585
+ `Website: ${data.website || 'None'}\n` +
586
+ `Location: ${data.location || 'Not specified'}\n` +
587
+ `Created: ${data.created_on}`,
588
+ },
589
+ ],
590
+ };
591
+ }
592
+ case 'bb_search_repositories': {
593
+ const parsed = SearchRepositoriesSchema.parse(args);
594
+ // Bitbucket API v2.0 doesn't have workspace-scoped repository search
595
+ // Instead, list all repositories in workspace and filter client-side
596
+ const params = {
597
+ page: parsed.page,
598
+ pagelen: parsed.pagelen,
599
+ };
600
+ const url = addQueryParams(buildApiUrl(`/repositories/${parsed.workspace}`), params);
601
+ const data = await makeRequest(url);
602
+ // Filter repositories based on search query
603
+ const searchLower = parsed.query.toLowerCase();
604
+ const filteredRepos = data.values.filter((repo) => {
605
+ const nameMatch = repo.name.toLowerCase().includes(searchLower);
606
+ const descMatch = repo.description?.toLowerCase().includes(searchLower) || false;
607
+ const fullNameMatch = repo.full_name
608
+ .toLowerCase()
609
+ .includes(searchLower);
610
+ return nameMatch || descMatch || fullNameMatch;
611
+ });
612
+ const repoList = filteredRepos
613
+ .map((repo) => `- ${repo.full_name} (${repo.language || 'Unknown'})\n` +
614
+ ` ${repo.description || 'No description'}\n` +
615
+ ` Private: ${repo.is_private}, Updated: ${repo.updated_on}`)
616
+ .join('\n\n');
617
+ return {
618
+ content: [
619
+ {
620
+ type: 'text',
621
+ text: `Search results for "${parsed.query}" in ${parsed.workspace} (${filteredRepos.length} of ${data.values.length} total):\n\n${repoList || 'No repositories found matching the search query.'}`,
622
+ },
623
+ ],
624
+ };
625
+ }
626
+ case 'bb_search_code': {
627
+ const parsed = SearchCodeSchema.parse(args);
628
+ const params = {
629
+ search_query: parsed.search_query,
630
+ page: parsed.page,
631
+ pagelen: parsed.pagelen,
632
+ };
633
+ if (parsed.repo_slug) {
634
+ params.search_query += ` repo:${parsed.repo_slug}`;
635
+ }
636
+ if (parsed.language) {
637
+ params.search_query += ` language:${parsed.language}`;
638
+ }
639
+ if (parsed.extension) {
640
+ params.search_query += ` extension:${parsed.extension}`;
641
+ }
642
+ const url = addQueryParams(buildApiUrl(`/workspaces/${parsed.workspace}/search/code`), params);
643
+ const data = await makeRequest(url);
644
+ const resultList = data.values
645
+ .map(result => {
646
+ const matchCount = result.content_match_count;
647
+ const filePath = result.file.path;
648
+ const matches = result.content_matches
649
+ .map(match => match.lines
650
+ .map(line => {
651
+ const lineText = line.segments
652
+ .map(seg => (seg.match ? `**${seg.text}**` : seg.text))
653
+ .join('');
654
+ return ` Line ${line.line}: ${lineText}`;
655
+ })
656
+ .join('\n'))
657
+ .join('\n');
658
+ return `📄 ${filePath} (${matchCount} matches)\n${matches}`;
659
+ })
660
+ .join('\n\n');
661
+ return {
662
+ content: [
663
+ {
664
+ type: 'text',
665
+ text: `Code search results for "${parsed.search_query}" in ${parsed.workspace} (${data.values.length} results):\n\n${resultList}`,
666
+ },
667
+ ],
668
+ };
669
+ }
670
+ case 'bb_get_workspace': {
671
+ const parsed = GetWorkspaceSchema.parse(args);
672
+ const url = buildApiUrl(`/workspaces/${parsed.workspace}`);
673
+ const data = await makeRequest(url);
674
+ return {
675
+ content: [
676
+ {
677
+ type: 'text',
678
+ text: `Workspace: ${data.name} (${data.slug})\n` +
679
+ `Type: ${data.type}\n` +
680
+ `UUID: ${data.uuid || 'Not available'}\n` +
681
+ `Created: ${data.created_on || 'Unknown'}`,
682
+ },
683
+ ],
684
+ };
685
+ }
686
+ default:
687
+ return {
688
+ content: [
689
+ {
690
+ type: 'text',
691
+ text: `Unknown tool: ${name}`,
692
+ },
693
+ ],
694
+ isError: true,
695
+ };
696
+ }
697
+ }
698
+ catch (error) {
699
+ return {
700
+ content: [
701
+ {
702
+ type: 'text',
703
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
704
+ },
705
+ ],
706
+ isError: true,
707
+ };
708
+ }
709
+ }
710
+ //# sourceMappingURL=tools.js.map