@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 +4 -0
- package/.envrc +13 -0
- package/README.md +60 -0
- package/dist/handlers/resources.js +104 -0
- package/dist/handlers/tools.js +478 -0
- package/dist/index.js +71 -0
- package/flake.lock +312 -0
- package/flake.nix +39 -0
- package/package.json +28 -0
- package/scripts/test-tools.js +139 -0
- package/scripts/test-tools.ts +92 -0
- package/scripts/verify-auth.ts +59 -0
- package/scripts/verify-defaults.ts +105 -0
- package/src/handlers/resources.ts +115 -0
- package/src/handlers/tools.ts +515 -0
- package/src/index.ts +81 -0
- package/tsconfig.json +15 -0
package/.env.example
ADDED
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
|
+
}
|