@submergedas/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/.env.example ADDED
@@ -0,0 +1,4 @@
1
+ BITBUCKET_USERNAME=your_username
2
+ BITBUCKET_API_TOKEN=your_api_token
3
+ BITBUCKET_WORKSPACE=your_workspace
4
+ BITBUCKET_REPO_SLUG=your_repo_slug
package/.envrc ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+
3
+ if ! has nix_direnv_version || ! nix_direnv_version 3.1.0; then
4
+ source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.0/direnvrc" "sha256-yMJ2OVMzrFaDPn7q8nCBZFRYpL/f0RcHzhmw/i6btJM="
5
+ fi
6
+
7
+ export DEVENV_IN_DIRENV_SHELL=true
8
+
9
+ watch_file flake.nix
10
+ watch_file flake.lock
11
+ if ! use flake . --no-pure-eval; then
12
+ echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
13
+ fi
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Bitbucket MCP Server
2
+
3
+ An MCP server that provides access to Bitbucket repositories and pull requests.
4
+
5
+ ## Features
6
+
7
+ ### Resources
8
+ - Read file content from repositories: `bitbucket://{workspace}/{repo_slug}/src/{commit}/{path}`
9
+
10
+ ### Tools
11
+ - `list_repositories`: List repositories in a workspace.
12
+ - `list_pull_requests`: List pull requests for a repository.
13
+ - `get_pull_request`: Get details of a specific pull request.
14
+ - `create_pull_request`: Create a new pull request.
15
+ - `read_file`: Read content of a specific file.
16
+ - `write_file`: Create or update a file's content.
17
+ - `delete_file`: Delete a file.
18
+ - `list_files`: List files in a directory.
19
+
20
+ ## Setup
21
+
22
+ 1. **Install dependencies**:
23
+ ```bash
24
+ npm install
25
+ ```
26
+
27
+ 2. **Build**:
28
+ ```bash
29
+ npm run build
30
+ ```
31
+ (You may need to add `"build": "tsc"` to `package.json` scripts if not present, checking project config...)
32
+ *Note: I ran `npx tsc` manually during setup.*
33
+
34
+ 3. **Configure Environment Variables**:
35
+ Create a `.env` file based on `.env.example`:
36
+ ```env
37
+ BITBUCKET_USERNAME=your_username
38
+ BITBUCKET_API_TOKEN=your_api_token
39
+ ```
40
+
41
+ > **Important:** `BITBUCKET_API_TOKEN` is required. You can create one in Bitbucket Settings -> Personal Bitbucket Settings -> API tokens.
42
+
43
+ ## Usage
44
+
45
+ To run the server with an MCP client (like Claude Desktop or similar):
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "bitbucket": {
51
+ "command": "node",
52
+ "args": ["/path/to/bitbucket-MCP/dist/index.js"],
53
+ "env": {
54
+ "BITBUCKET_USERNAME": "your_username",
55
+ "BITBUCKET_API_TOKEN": "your_api_token"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerResources = registerResources;
4
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
5
+ function registerResources(server, bitbucket) {
6
+ server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
7
+ // Bitbucket API doesn't have a direct "list all available resources" concept that maps 1:1 to MCP resources list
8
+ // efficiently without context. However, we can list repositories for the authenticated user/workspace
9
+ // if we had the context of which workspace to list for.
10
+ // For now, we'll return an empty list or maybe a static list if appropriate,
11
+ // but the previous implementation didn't seem to list resources globally either,
12
+ // it just defined patterns.
13
+ // Actually, looking at previous code:
14
+ // server.resource("repositories", new ResourceTemplate("bitbucket://{workspace}/repositories", ...))
15
+ // server.resource("file-content", new ResourceTemplate("bitbucket://{workspace}/{repo_slug}/src/{commit}/{path}", ...))
16
+ // The "list" capability in the new API is for listing concrete resources, not templates.
17
+ // If we want to expose templates, we might need a different approach or just not list them if they are dynamic.
18
+ // But RequestHandler for ListResourcesRequestSchema is expected.
19
+ return {
20
+ resources: [],
21
+ };
22
+ });
23
+ server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
24
+ const uri = new URL(request.params.uri);
25
+ // Expected Patterns:
26
+ // bitbucket://{workspace}/repositories
27
+ // bitbucket://{workspace}/{repo_slug}/src/{commit}/{path}
28
+ const pathParts = uri.pathname.replace(/^\//, '').split('/');
29
+ const host = uri.host; // workspace
30
+ // Pattern 1: bitbucket://{workspace}/repositories
31
+ if (uri.protocol === "bitbucket:" && pathParts[0] === "repositories" && pathParts.length === 1) {
32
+ const workspace = host;
33
+ try {
34
+ const { data } = await bitbucket.repositories.list({
35
+ workspace,
36
+ pagelen: 50,
37
+ });
38
+ return {
39
+ contents: [
40
+ {
41
+ uri: request.params.uri,
42
+ mimeType: "application/json",
43
+ text: JSON.stringify(data.values?.map((repo) => ({
44
+ slug: repo.slug,
45
+ name: repo.name,
46
+ full_name: repo.full_name,
47
+ description: repo.description,
48
+ links: repo.links
49
+ })), null, 2),
50
+ },
51
+ ],
52
+ };
53
+ }
54
+ catch (error) {
55
+ throw new Error(`Failed to list repositories: ${error.message}`);
56
+ }
57
+ }
58
+ // Pattern 2: bitbucket://{workspace}/{repo_slug}/src/{commit}/{path}
59
+ // uri.pathname starts with /. so pathParts[0] is repo_slug, [1] is 'src', [2] is commit, [3+] is path
60
+ if (uri.protocol === "bitbucket:" && pathParts.length >= 4 && pathParts[1] === "src") {
61
+ const workspace = host;
62
+ const repo_slug = pathParts[0];
63
+ const commit = pathParts[2];
64
+ const filePath = pathParts.slice(3).join('/');
65
+ try {
66
+ const { data } = await bitbucket.source.read({
67
+ workspace,
68
+ repo_slug,
69
+ commit,
70
+ path: filePath,
71
+ });
72
+ // Bitbucket might return text or binary. simpler to assume text for now or handle appropriately.
73
+ // If it's a directory listing, data will be a paginated response.
74
+ if (typeof data === "string") {
75
+ return {
76
+ contents: [
77
+ {
78
+ uri: request.params.uri,
79
+ text: data,
80
+ },
81
+ ],
82
+ };
83
+ }
84
+ else {
85
+ // It might be a directory or binary.
86
+ // For simplicity, we'll serialize JSON if it's an object (directory listing)
87
+ return {
88
+ contents: [
89
+ {
90
+ uri: request.params.uri,
91
+ mimeType: "application/json",
92
+ text: JSON.stringify(data, null, 2),
93
+ },
94
+ ],
95
+ };
96
+ }
97
+ }
98
+ catch (error) {
99
+ throw new Error(`Failed to read file: ${error.message}`);
100
+ }
101
+ }
102
+ throw new Error(`Resource not found: ${request.params.uri}`);
103
+ });
104
+ }
@@ -0,0 +1,478 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTools = registerTools;
4
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
5
+ const zod_1 = require("zod");
6
+ const zod_to_json_schema_1 = require("zod-to-json-schema");
7
+ const child_process_1 = require("child_process");
8
+ const util_1 = require("util");
9
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
10
+ function registerTools(server, bitbucket, config) {
11
+ const { defaultWorkspace, defaultRepoSlug } = config;
12
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
13
+ return {
14
+ tools: [
15
+ {
16
+ name: "list_repositories",
17
+ description: "List repositories in a workspace",
18
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
19
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
20
+ })),
21
+ },
22
+ {
23
+ name: "list_pull_requests",
24
+ description: "List pull requests for a repository",
25
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
26
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
27
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
28
+ state: zod_1.z.enum(["OPEN", "MERGED", "DECLINED", "SUPERSEDED"]).optional().describe("Filter by PR state (default: OPEN)"),
29
+ })),
30
+ },
31
+ {
32
+ name: "get_pull_request",
33
+ description: "Get details of a specific pull request",
34
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
35
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
36
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
37
+ pull_request_id: zod_1.z.number().describe("The pull request ID"),
38
+ })),
39
+ },
40
+ {
41
+ name: "create_pull_request",
42
+ description: "Create a new pull request",
43
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
44
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
45
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
46
+ title: zod_1.z.string().optional().describe("Title of the pull request (default: generated from branches)"),
47
+ source_branch: zod_1.z.string().describe("Name of the source branch"),
48
+ destination_branch: zod_1.z.string().default("main").describe("Name of the destination branch"),
49
+ description: zod_1.z.string().optional().describe("Description of the pull request"),
50
+ })),
51
+ },
52
+ {
53
+ name: "read_file",
54
+ description: "Read content of a specific file",
55
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
56
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
57
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
58
+ path: zod_1.z.string().describe("Path to the file"),
59
+ commit: zod_1.z.string().optional().describe("Commit hash or branch name (defaults to main/master)"),
60
+ })),
61
+ },
62
+ {
63
+ name: "list_files",
64
+ description: "List files in a directory",
65
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
66
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
67
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
68
+ path: zod_1.z.string().describe("Path to the directory (empty for root)"),
69
+ commit: zod_1.z.string().optional().describe("Commit hash or branch name"),
70
+ })),
71
+ },
72
+ {
73
+ name: "write_file",
74
+ description: "Create or update a file's content",
75
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
76
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
77
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
78
+ path: zod_1.z.string().describe("Path to the file"),
79
+ content: zod_1.z.string().describe("Content of the file"),
80
+ message: zod_1.z.string().describe("Commit message"),
81
+ branch: zod_1.z.string().optional().describe("Branch to commit to (defaults to repository's default branch)"),
82
+ })),
83
+ },
84
+ {
85
+ name: "delete_file",
86
+ description: "Delete a file",
87
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
88
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
89
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
90
+ path: zod_1.z.string().describe("Path to the file to delete"),
91
+ message: zod_1.z.string().describe("Commit message"),
92
+ branch: zod_1.z.string().optional().describe("Branch to commit to"),
93
+ })),
94
+ },
95
+ {
96
+ name: "merge_pull_request",
97
+ description: "Merge a pull request",
98
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
99
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
100
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
101
+ pull_request_id: zod_1.z.number().describe("The pull request ID"),
102
+ message: zod_1.z.string().optional().describe("Commit message"),
103
+ close_source_branch: zod_1.z.boolean().optional().describe("Whether to close the source branch"),
104
+ merge_strategy: zod_1.z.enum(["merge_commit", "squash", "fast_forward"]).optional().describe("Merge strategy"),
105
+ })),
106
+ },
107
+ {
108
+ name: "create_issue",
109
+ description: "Create a new issue",
110
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
111
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
112
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
113
+ title: zod_1.z.string().describe("Title of the issue"),
114
+ description: zod_1.z.string().optional().describe("Description of the issue"),
115
+ kind: zod_1.z.enum(["bug", "enhancement", "proposal", "task"]).optional().describe("Kind of issue"),
116
+ priority: zod_1.z.enum(["trivial", "minor", "major", "critical", "blocker"]).optional().describe("Priority of issue"),
117
+ })),
118
+ },
119
+ {
120
+ name: "list_issues",
121
+ description: "List issues in a repository",
122
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
123
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
124
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
125
+ state: zod_1.z.enum(["new", "open", "resolved", "on hold", "invalid", "duplicate", "wontfix", "closed"]).optional().describe("Filter by issue state"),
126
+ })),
127
+ },
128
+ {
129
+ name: "get_issue",
130
+ description: "Get details of a specific issue",
131
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
132
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
133
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
134
+ issue_id: zod_1.z.string().describe("The issue ID"),
135
+ })),
136
+ },
137
+ {
138
+ name: "git_clone",
139
+ description: "Clone a repository (requires git on the server path)",
140
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({
141
+ workspace: zod_1.z.string().optional().describe(`The workspace slug/ID (default: ${defaultWorkspace || "required"})`),
142
+ repo_slug: zod_1.z.string().optional().describe(`The repository slug (default: ${defaultRepoSlug || "required"})`),
143
+ destination: zod_1.z.string().optional().describe("Local path to clone into (defaults to current directory + repo_slug)"),
144
+ })),
145
+ },
146
+ ],
147
+ };
148
+ });
149
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
150
+ const { name, arguments: args } = request.params;
151
+ // Helper to get workspace/repo or throw
152
+ const getContext = (args) => {
153
+ const workspace = args?.workspace || defaultWorkspace;
154
+ const repo_slug = args?.repo_slug || defaultRepoSlug;
155
+ if (!workspace)
156
+ throw new Error("Workspace is required (no default set)");
157
+ if (!repo_slug)
158
+ throw new Error("Repository slug is required (no default set)");
159
+ return { workspace, repo_slug };
160
+ };
161
+ try {
162
+ if (name === "list_repositories") {
163
+ const workspace = args?.workspace || defaultWorkspace;
164
+ if (!workspace)
165
+ throw new Error("Workspace is required");
166
+ const { data } = await bitbucket.repositories.list({
167
+ workspace,
168
+ pagelen: 50,
169
+ });
170
+ return {
171
+ content: [
172
+ {
173
+ type: "text",
174
+ text: JSON.stringify(data.values?.map((repo) => ({
175
+ slug: repo.slug,
176
+ name: repo.name,
177
+ full_name: repo.full_name,
178
+ description: repo.description,
179
+ links: repo.links
180
+ })), null, 2),
181
+ },
182
+ ],
183
+ };
184
+ }
185
+ if (name === "list_pull_requests") {
186
+ const { workspace, repo_slug } = getContext(args);
187
+ const { state } = args;
188
+ const { data } = await bitbucket.pullrequests.list({
189
+ workspace,
190
+ repo_slug,
191
+ state: state || "OPEN",
192
+ });
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: JSON.stringify(data.values?.map((pr) => ({
198
+ id: pr.id,
199
+ title: pr.title,
200
+ state: pr.state,
201
+ author: pr.author?.display_name,
202
+ created_on: pr.created_on,
203
+ updated_on: pr.updated_on,
204
+ links: pr.links
205
+ })), null, 2),
206
+ },
207
+ ],
208
+ };
209
+ }
210
+ if (name === "get_pull_request") {
211
+ const { workspace, repo_slug } = getContext(args);
212
+ const { pull_request_id } = args;
213
+ const { data } = await bitbucket.pullrequests.get({
214
+ workspace,
215
+ repo_slug,
216
+ pull_request_id,
217
+ });
218
+ return {
219
+ content: [
220
+ {
221
+ type: "text",
222
+ text: JSON.stringify(data, null, 2),
223
+ },
224
+ ],
225
+ };
226
+ }
227
+ if (name === "create_pull_request") {
228
+ const { workspace, repo_slug } = getContext(args);
229
+ const { source_branch, destination_branch, description } = args;
230
+ let { title } = args;
231
+ if (!title) {
232
+ title = `Merge ${source_branch} into ${destination_branch}`;
233
+ }
234
+ const { data } = await bitbucket.pullrequests.create({
235
+ workspace,
236
+ repo_slug,
237
+ _body: {
238
+ title,
239
+ description,
240
+ source: {
241
+ branch: {
242
+ name: source_branch
243
+ }
244
+ },
245
+ destination: {
246
+ branch: {
247
+ name: destination_branch
248
+ }
249
+ }
250
+ }
251
+ });
252
+ return {
253
+ content: [
254
+ {
255
+ type: "text",
256
+ text: JSON.stringify(data, null, 2),
257
+ },
258
+ ],
259
+ };
260
+ }
261
+ if (name === "read_file") {
262
+ const { workspace, repo_slug } = getContext(args);
263
+ const { path, commit } = args;
264
+ const { data } = await bitbucket.source.read({
265
+ workspace,
266
+ repo_slug,
267
+ path,
268
+ commit,
269
+ });
270
+ if (typeof data === "string") {
271
+ return {
272
+ content: [
273
+ {
274
+ type: "text",
275
+ text: data,
276
+ },
277
+ ],
278
+ };
279
+ }
280
+ else {
281
+ return {
282
+ content: [
283
+ {
284
+ type: "text",
285
+ text: JSON.stringify(data, null, 2),
286
+ },
287
+ ],
288
+ };
289
+ }
290
+ }
291
+ if (name === "list_files") {
292
+ const { workspace, repo_slug } = getContext(args);
293
+ const { path, commit } = args;
294
+ const { data } = await bitbucket.source.read({
295
+ workspace,
296
+ repo_slug,
297
+ path: path || "",
298
+ commit,
299
+ });
300
+ return {
301
+ content: [
302
+ {
303
+ type: "text",
304
+ text: JSON.stringify(data, null, 2),
305
+ },
306
+ ],
307
+ };
308
+ }
309
+ if (name === "write_file") {
310
+ const { workspace, repo_slug } = getContext(args);
311
+ const { path, content, message, branch } = args;
312
+ const body = {};
313
+ body[path] = content;
314
+ const { data } = await bitbucket.repositories.createSrcFileCommit({
315
+ workspace,
316
+ repo_slug,
317
+ message,
318
+ branch,
319
+ _body: body,
320
+ });
321
+ return {
322
+ content: [
323
+ {
324
+ type: "text",
325
+ text: "File written successfully",
326
+ },
327
+ ],
328
+ };
329
+ }
330
+ if (name === "delete_file") {
331
+ const { workspace, repo_slug } = getContext(args);
332
+ const { path, message, branch } = args;
333
+ const { data } = await bitbucket.repositories.createSrcFileCommit({
334
+ workspace,
335
+ repo_slug,
336
+ message,
337
+ branch,
338
+ files: path,
339
+ });
340
+ return {
341
+ content: [
342
+ {
343
+ type: "text",
344
+ text: "File deleted successfully",
345
+ },
346
+ ],
347
+ };
348
+ }
349
+ if (name === "merge_pull_request") {
350
+ const { workspace, repo_slug } = getContext(args);
351
+ const { pull_request_id, message, close_source_branch, merge_strategy } = args;
352
+ const { data } = await bitbucket.repositories.mergePullRequest({
353
+ workspace,
354
+ repo_slug,
355
+ pull_request_id,
356
+ _body: {
357
+ message,
358
+ close_source_branch,
359
+ merge_strategy,
360
+ },
361
+ });
362
+ return {
363
+ content: [
364
+ {
365
+ type: "text",
366
+ text: JSON.stringify(data, null, 2),
367
+ },
368
+ ],
369
+ };
370
+ }
371
+ if (name === "create_issue") {
372
+ const { workspace, repo_slug } = getContext(args);
373
+ const { title, description, kind, priority } = args;
374
+ const { data } = await bitbucket.issue_tracker.create({
375
+ workspace,
376
+ repo_slug,
377
+ _body: {
378
+ title,
379
+ content: {
380
+ raw: description || "",
381
+ },
382
+ kind,
383
+ priority,
384
+ },
385
+ });
386
+ return {
387
+ content: [
388
+ {
389
+ type: "text",
390
+ text: JSON.stringify(data, null, 2),
391
+ },
392
+ ],
393
+ };
394
+ }
395
+ if (name === "list_issues") {
396
+ const { workspace, repo_slug } = getContext(args);
397
+ const { state } = args;
398
+ const q = state ? `state="${state}"` : undefined;
399
+ const { data } = await bitbucket.issue_tracker.list({
400
+ workspace,
401
+ repo_slug,
402
+ q,
403
+ });
404
+ return {
405
+ content: [
406
+ {
407
+ type: "text",
408
+ text: JSON.stringify(data.values?.map((issue) => ({
409
+ id: issue.id,
410
+ title: issue.title,
411
+ state: issue.state,
412
+ kind: issue.kind,
413
+ priority: issue.priority,
414
+ created_on: issue.created_on,
415
+ updated_on: issue.updated_on,
416
+ links: issue.links
417
+ })), null, 2),
418
+ },
419
+ ],
420
+ };
421
+ }
422
+ if (name === "git_clone") {
423
+ const { workspace, repo_slug } = getContext(args);
424
+ const { destination } = args;
425
+ // Note: bitbucket.proxy is not a public property, so we are accessing private credentials here effectively.
426
+ // However, the `bitbucket` instance doesn't easily expose the raw credentials.
427
+ // We will fallback to process.env since that's how we initialized it.
428
+ const username = process.env.BITBUCKET_USERNAME;
429
+ const appPassword = process.env.BITBUCKET_APP_PASSWORD;
430
+ const apiToken = process.env.BITBUCKET_API_TOKEN;
431
+ const password = apiToken || appPassword;
432
+ if (!username || !password) {
433
+ throw new Error("Missing BITBUCKET_USERNAME or BITBUCKET_API_TOKEN/APP_PASSWORD in environment variables");
434
+ }
435
+ const authUrl = `https://${username}:${password}@bitbucket.org/${workspace}/${repo_slug}.git`;
436
+ const destPath = destination || repo_slug; // Default to repo_slug if no destination provided
437
+ await execAsync(`git clone ${authUrl} ${destPath}`);
438
+ return {
439
+ content: [
440
+ {
441
+ type: "text",
442
+ text: `Successfully cloned ${workspace}/${repo_slug} into ${destPath}`,
443
+ },
444
+ ],
445
+ };
446
+ }
447
+ if (name === "get_issue") {
448
+ const { workspace, repo_slug } = getContext(args);
449
+ const { issue_id } = args;
450
+ const { data } = await bitbucket.issue_tracker.get({
451
+ workspace,
452
+ repo_slug,
453
+ issue_id,
454
+ });
455
+ return {
456
+ content: [
457
+ {
458
+ type: "text",
459
+ text: JSON.stringify(data, null, 2),
460
+ },
461
+ ],
462
+ };
463
+ }
464
+ throw new Error(`Tool not found: ${name}`);
465
+ }
466
+ catch (error) {
467
+ return {
468
+ content: [
469
+ {
470
+ type: "text",
471
+ text: `Error: ${error.message}`,
472
+ },
473
+ ],
474
+ isError: true,
475
+ };
476
+ }
477
+ });
478
+ }