@stubbedev/atlassian-mcp 0.0.4 → 0.1.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/README.md +36 -9
- package/dist/bitbucket.js +236 -17
- package/dist/context.js +0 -26
- package/dist/index.js +196 -143
- package/dist/jira.js +97 -12
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# atlassian-mcp
|
|
2
2
|
|
|
3
|
-
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **self-hosted Jira** (Server / Data Center) and **self-hosted Bitbucket** (Server / Data Center). Exposes
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **self-hosted Jira** (Server / Data Center) and **self-hosted Bitbucket** (Server / Data Center). Exposes 33 tools for natural-language workflows around tickets, pull requests, review threads, and git context.
|
|
4
4
|
|
|
5
5
|
> **Note:** This server only supports self-hosted instances. Jira Cloud and Bitbucket Cloud use different APIs and are not supported.
|
|
6
6
|
|
|
@@ -12,13 +12,13 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
12
12
|
|
|
13
13
|
| Tool | Description |
|
|
14
14
|
|---|---|
|
|
15
|
-
| `get_dev_context` |
|
|
15
|
+
| `get_dev_context` | One-shot coding context: local git state + linked Jira tickets + open PR for current branch |
|
|
16
16
|
|
|
17
17
|
### Jira
|
|
18
18
|
|
|
19
19
|
| Tool | Description |
|
|
20
20
|
|---|---|
|
|
21
|
-
| `jira_search_issues` |
|
|
21
|
+
| `jira_search_issues` | Find tickets by plain language, JQL, project, status, assignee, or type |
|
|
22
22
|
| `jira_my_issues` | List issues assigned to you, ordered by last updated |
|
|
23
23
|
| `jira_get_projects` | List all accessible projects |
|
|
24
24
|
| `jira_get_issue_types` | List issue types and their available statuses for a project |
|
|
@@ -28,25 +28,24 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
28
28
|
| `jira_search_users` | Search for users by name or email |
|
|
29
29
|
| `jira_get_comments` | List comments on an issue |
|
|
30
30
|
| `jira_add_comment` | Add a comment to an issue |
|
|
31
|
-
| `
|
|
32
|
-
| `jira_transition_issue` | Change issue status via transition ID |
|
|
31
|
+
| `jira_transition_issue` | Move issue status via transition name or transition ID |
|
|
33
32
|
|
|
34
33
|
### Bitbucket
|
|
35
34
|
|
|
36
35
|
| Tool | Description |
|
|
37
36
|
|---|---|
|
|
38
37
|
| `bitbucket_list_repos` | List repositories (optionally by project) |
|
|
39
|
-
| `bitbucket_list_pull_requests` | List pull requests
|
|
38
|
+
| `bitbucket_list_pull_requests` | List repository pull requests (filter by state, source branch, or text) |
|
|
40
39
|
| `bitbucket_my_prs` | List PRs in your inbox (authored by you or awaiting review) |
|
|
41
40
|
| `bitbucket_get_pull_request` | Get pull request details |
|
|
41
|
+
| `bitbucket_get_pr_overview` | Get a one-call PR overview: metadata, commits, comments/blockers, and optional diff |
|
|
42
42
|
| `bitbucket_get_pr_diff` | Get the code diff for a pull request |
|
|
43
43
|
| `bitbucket_create_pull_request` | Create a new pull request |
|
|
44
|
-
| `bitbucket_create_pr_from_context` | Create a PR auto-detecting project, repo, and branch from the current git repo |
|
|
45
44
|
| `bitbucket_approve_pr` | Approve a pull request |
|
|
46
45
|
| `bitbucket_unapprove_pr` | Remove your approval from a pull request |
|
|
47
46
|
| `bitbucket_merge_pr` | Merge a pull request |
|
|
48
47
|
| `bitbucket_decline_pr` | Decline a pull request |
|
|
49
|
-
| `bitbucket_get_pr_comments` | Get comment threads
|
|
48
|
+
| `bitbucket_get_pr_comments` | Get PR comment threads in bulk, including blocker comments and blocker counts |
|
|
50
49
|
| `bitbucket_add_pr_comment` | Add a top-level PR comment or reply to an existing comment |
|
|
51
50
|
| `bitbucket_update_pr_comment` | Update comment text, state, or severity (`NORMAL` / `BLOCKER`) |
|
|
52
51
|
| `bitbucket_delete_pr_comment` | Delete a PR comment by comment ID |
|
|
@@ -64,6 +63,18 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
64
63
|
|
|
65
64
|
All list tools support `limit` and `start`/`startAt` for pagination.
|
|
66
65
|
|
|
66
|
+
### Natural language examples
|
|
67
|
+
|
|
68
|
+
- "show my PRs waiting for review" → `bitbucket_my_prs`
|
|
69
|
+
- "list open PRs for this repo from branch feature/ABC-123" → `bitbucket_list_pull_requests`
|
|
70
|
+
- "open a PR from my current branch to master" → `bitbucket_create_pull_request`
|
|
71
|
+
- "show review comments on PR 42" → `bitbucket_get_pr_comments`
|
|
72
|
+
- "give me one full overview of PR 42" → `bitbucket_get_pr_overview`
|
|
73
|
+
- "how many open blockers are on PR 42" → `bitbucket_get_pr_comments` with `severity=BLOCKER` and `countOnly=true`
|
|
74
|
+
- "move FOO-123 to In Progress" → `jira_transition_issue` with `transitionName="In Progress"`
|
|
75
|
+
- "find bugs assigned to me in PAY project" → `jira_search_issues`
|
|
76
|
+
- "give me my coding context for this branch" → `get_dev_context`
|
|
77
|
+
|
|
67
78
|
---
|
|
68
79
|
|
|
69
80
|
## Setup
|
|
@@ -86,7 +97,18 @@ Create `~/.atlassian-mcp.json`:
|
|
|
86
97
|
}
|
|
87
98
|
```
|
|
88
99
|
|
|
89
|
-
The `$schema` field is optional but enables editor autocomplete and validation.
|
|
100
|
+
The `$schema` field is optional but enables editor autocomplete and validation.
|
|
101
|
+
|
|
102
|
+
- `projectKey` means a project code:
|
|
103
|
+
- Jira example: `PAY` in ticket `PAY-123`
|
|
104
|
+
- Bitbucket example: project `ENG` in repo path `ENG/payments-service`
|
|
105
|
+
- You can also use ergonomic aliases:
|
|
106
|
+
- Jira: `project` (alias of `projectKey`)
|
|
107
|
+
- Bitbucket: `project` and `repo` (aliases of `projectKey` and `repoSlug`)
|
|
108
|
+
- For Bitbucket tools, `projectKey` and `repoSlug` are usually auto-detected from your local `origin` remote.
|
|
109
|
+
- `bitbucket_create_pull_request` also auto-detects `fromBranch` from your current branch.
|
|
110
|
+
- Jira project-scoped calls accept `projectKey` and work best when provided.
|
|
111
|
+
- If `projectKey` is omitted for Jira issue creation/type lookup, the server tries to infer it from your current branch ticket key, falls back to auto-select when only one project is visible, and otherwise returns a numbered project list to pick from.
|
|
90
112
|
|
|
91
113
|
Alternatively, use environment variables (or a `.env` file in this directory):
|
|
92
114
|
|
|
@@ -237,6 +259,8 @@ Then use `node /path/to/atlassian-mcp/dist/index.js` instead of the `npx` comman
|
|
|
237
259
|
|
|
238
260
|
This package is published to npm as `@stubbedev/atlassian-mcp`.
|
|
239
261
|
|
|
262
|
+
Use semantic versioning for releases. Breaking tool-surface changes should bump the minor version while `<1.0.0` (for example `0.0.x` -> `0.1.0`).
|
|
263
|
+
|
|
240
264
|
Automatic publish is configured in `.github/workflows/publish.yml`:
|
|
241
265
|
|
|
242
266
|
- Push a tag like `v1.0.1` to publish from CI
|
|
@@ -304,6 +328,9 @@ node dist/index.js
|
|
|
304
328
|
|
|
305
329
|
# Test the tool list
|
|
306
330
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node dist/index.js
|
|
331
|
+
|
|
332
|
+
# Quick release smoke check
|
|
333
|
+
npm run smoke
|
|
307
334
|
```
|
|
308
335
|
|
|
309
336
|
To use a specific config file:
|
package/dist/bitbucket.js
CHANGED
|
@@ -49,6 +49,29 @@ function formatCommentThread(comment, indent = '') {
|
|
|
49
49
|
}
|
|
50
50
|
return lines;
|
|
51
51
|
}
|
|
52
|
+
function commentMatchesState(comment, state) {
|
|
53
|
+
const currentState = comment.state ?? 'OPEN';
|
|
54
|
+
if (currentState === state)
|
|
55
|
+
return true;
|
|
56
|
+
return (comment.comments ?? []).some((child) => commentMatchesState(child, state));
|
|
57
|
+
}
|
|
58
|
+
function commentMatchesSeverity(comment, severity) {
|
|
59
|
+
if (severity === 'ALL')
|
|
60
|
+
return true;
|
|
61
|
+
const currentSeverity = comment.severity ?? 'NORMAL';
|
|
62
|
+
if (currentSeverity === severity)
|
|
63
|
+
return true;
|
|
64
|
+
return (comment.comments ?? []).some((child) => commentMatchesSeverity(child, severity));
|
|
65
|
+
}
|
|
66
|
+
function uniqueCommentsFromActivities(activities) {
|
|
67
|
+
const byId = new Map();
|
|
68
|
+
for (const activity of activities) {
|
|
69
|
+
if (activity.action === 'COMMENTED' && activity.comment && !byId.has(activity.comment.id)) {
|
|
70
|
+
byId.set(activity.comment.id, activity.comment);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return Array.from(byId.values()).sort((a, b) => (a.createdDate ?? 0) - (b.createdDate ?? 0));
|
|
74
|
+
}
|
|
52
75
|
function pageHint(data) {
|
|
53
76
|
return data.isLastPage ? '' : ` (use start=${data.nextPageStart} for next page)`;
|
|
54
77
|
}
|
|
@@ -75,6 +98,44 @@ function formatDiff(data, maxChars = 8000) {
|
|
|
75
98
|
}
|
|
76
99
|
return result;
|
|
77
100
|
}
|
|
101
|
+
function parseBitbucketErrorDetails(errText) {
|
|
102
|
+
const trimmed = errText.trim();
|
|
103
|
+
if (!trimmed)
|
|
104
|
+
return '';
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(trimmed);
|
|
107
|
+
if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
|
|
108
|
+
const messages = parsed.errors
|
|
109
|
+
.map((e) => {
|
|
110
|
+
const msg = e.message?.trim() ?? '';
|
|
111
|
+
if (!msg)
|
|
112
|
+
return '';
|
|
113
|
+
return e.context ? `${e.context}: ${msg}` : msg;
|
|
114
|
+
})
|
|
115
|
+
.filter((m) => m.length > 0);
|
|
116
|
+
if (messages.length > 0)
|
|
117
|
+
return messages.join(' | ');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Fallback to raw text below
|
|
122
|
+
}
|
|
123
|
+
return trimmed.length > 500 ? `${trimmed.slice(0, 500)}...` : trimmed;
|
|
124
|
+
}
|
|
125
|
+
function formatBitbucketError(status, method, path, details) {
|
|
126
|
+
const prefix = `Bitbucket ${status} ${method} ${path}`;
|
|
127
|
+
if (status === 400)
|
|
128
|
+
return `${prefix}. Invalid request or parameters. ${details}`.trim();
|
|
129
|
+
if (status === 401)
|
|
130
|
+
return `${prefix}. Authentication failed. Check BITBUCKET_ACCESS_TOKEN.`;
|
|
131
|
+
if (status === 403)
|
|
132
|
+
return `${prefix}. Permission denied. Check repository/project permissions for this token.`;
|
|
133
|
+
if (status === 404)
|
|
134
|
+
return `${prefix}. Resource not found. Verify project/repo/PR identifiers and access.`;
|
|
135
|
+
if (status === 409)
|
|
136
|
+
return `${prefix}. Conflict (often stale version/state). Refresh and retry. ${details}`.trim();
|
|
137
|
+
return details ? `${prefix}. ${details}` : prefix;
|
|
138
|
+
}
|
|
78
139
|
export class BitbucketClient {
|
|
79
140
|
baseUrl;
|
|
80
141
|
headers;
|
|
@@ -109,7 +170,8 @@ export class BitbucketClient {
|
|
|
109
170
|
const res = await fetch(url, opts);
|
|
110
171
|
if (!res.ok) {
|
|
111
172
|
const errText = await res.text();
|
|
112
|
-
|
|
173
|
+
const details = parseBitbucketErrorDetails(errText);
|
|
174
|
+
throw new Error(formatBitbucketError(res.status, method, path, details));
|
|
113
175
|
}
|
|
114
176
|
return res.status === 204 ? null : res.json();
|
|
115
177
|
}
|
|
@@ -121,7 +183,8 @@ export class BitbucketClient {
|
|
|
121
183
|
});
|
|
122
184
|
if (!res.ok) {
|
|
123
185
|
const errText = await res.text();
|
|
124
|
-
|
|
186
|
+
const details = parseBitbucketErrorDetails(errText);
|
|
187
|
+
throw new Error(formatBitbucketError(res.status, 'GET', path, details));
|
|
125
188
|
}
|
|
126
189
|
return res.text();
|
|
127
190
|
}
|
|
@@ -144,8 +207,13 @@ export class BitbucketClient {
|
|
|
144
207
|
}
|
|
145
208
|
async listPullRequests(args) {
|
|
146
209
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
147
|
-
const { state = 'OPEN', limit = 25, start = 0 } = args;
|
|
148
|
-
const
|
|
210
|
+
const { state = 'OPEN', fromBranch, text: searchText, limit = 25, start = 0 } = args;
|
|
211
|
+
const qs = new URLSearchParams({ state, limit: String(limit), start: String(start) });
|
|
212
|
+
if (fromBranch)
|
|
213
|
+
qs.set('at', toBranchRef(fromBranch));
|
|
214
|
+
if (searchText)
|
|
215
|
+
qs.set('filterText', searchText);
|
|
216
|
+
const path = `/projects/${projectKey}/repos/${repoSlug}/pull-requests?${qs}`;
|
|
149
217
|
const data = await this.request('GET', path);
|
|
150
218
|
if (!data || data.values.length === 0)
|
|
151
219
|
return text(`No ${state} pull requests found.`);
|
|
@@ -153,8 +221,11 @@ export class BitbucketClient {
|
|
|
153
221
|
return text(`${data.values.length} PR(s) (${state})${pageHint(data)}:\n${lines.join('\n')}`);
|
|
154
222
|
}
|
|
155
223
|
async myPrs(args) {
|
|
156
|
-
const { limit = 25, start = 0 } = args;
|
|
157
|
-
const
|
|
224
|
+
const { limit = 25, start = 0, role } = args;
|
|
225
|
+
const qs = new URLSearchParams({ limit: String(limit), start: String(start) });
|
|
226
|
+
if (role)
|
|
227
|
+
qs.set('role', role);
|
|
228
|
+
const data = await this.request('GET', `/inbox/pull-requests?${qs}`);
|
|
158
229
|
if (!data || data.values.length === 0)
|
|
159
230
|
return text('No pull requests in your inbox.');
|
|
160
231
|
const lines = data.values.map((pr) => {
|
|
@@ -183,6 +254,82 @@ export class BitbucketClient {
|
|
|
183
254
|
];
|
|
184
255
|
return text(lines.join('\n'));
|
|
185
256
|
}
|
|
257
|
+
async getPrOverview(args) {
|
|
258
|
+
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
259
|
+
const includeCommits = args.includeCommits ?? true;
|
|
260
|
+
const includeComments = args.includeComments ?? true;
|
|
261
|
+
const includeDiff = args.includeDiff ?? false;
|
|
262
|
+
const pr = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}`);
|
|
263
|
+
if (!pr)
|
|
264
|
+
return text('Pull request not found.');
|
|
265
|
+
const sections = [];
|
|
266
|
+
const reviewers = pr.reviewers.map((r) => `${r.user.displayName}${r.approved ? ' ✓' : ''}`).join(', ');
|
|
267
|
+
const url = pr.links?.self?.[0]?.href;
|
|
268
|
+
const header = [
|
|
269
|
+
`PR #${pr.id}: ${pr.title}`,
|
|
270
|
+
`State: ${pr.state}`,
|
|
271
|
+
`Author: ${pr.author.user.displayName}`,
|
|
272
|
+
`Branch: ${pr.fromRef.displayId} → ${pr.toRef.displayId}`,
|
|
273
|
+
`Reviewers: ${reviewers || 'None'}`,
|
|
274
|
+
url ? `URL: ${url}` : '',
|
|
275
|
+
'',
|
|
276
|
+
'Description:',
|
|
277
|
+
pr.description ?? '(no description)',
|
|
278
|
+
].filter((line) => line !== '');
|
|
279
|
+
sections.push(header.join('\n'));
|
|
280
|
+
if (includeCommits) {
|
|
281
|
+
const commitsLimit = args.commitsLimit ?? 25;
|
|
282
|
+
const commitsStart = args.commitsStart ?? 0;
|
|
283
|
+
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/commits?limit=${commitsLimit}&start=${commitsStart}`);
|
|
284
|
+
if (!data || data.values.length === 0) {
|
|
285
|
+
sections.push('Commits:\n(no commits found)');
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const lines = data.values.map((c) => `${c.displayId} ${formatDate(c.authorTimestamp)} ${c.author.name}: ${c.message.split('\n')[0]}`);
|
|
289
|
+
sections.push(`Commits (${data.values.length})${pageHint(data)}:\n${lines.join('\n')}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (includeComments) {
|
|
293
|
+
const commentsLimit = args.commentsLimit ?? 50;
|
|
294
|
+
const commentsStart = args.commentsStart ?? 0;
|
|
295
|
+
const commentsState = args.commentsState ?? 'OPEN';
|
|
296
|
+
const commentsSeverity = args.commentsSeverity ?? 'ALL';
|
|
297
|
+
if (commentsSeverity === 'BLOCKER' && commentsState === 'PENDING') {
|
|
298
|
+
throw new Error('commentsState=PENDING is not valid when commentsSeverity=BLOCKER. Use OPEN or RESOLVED.');
|
|
299
|
+
}
|
|
300
|
+
if (commentsSeverity === 'BLOCKER') {
|
|
301
|
+
const qs = new URLSearchParams({ limit: String(commentsLimit), start: String(commentsStart), state: commentsState });
|
|
302
|
+
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/blocker-comments?${qs}`);
|
|
303
|
+
if (!data || data.values.length === 0) {
|
|
304
|
+
sections.push(`Comments:\n(no ${commentsState} BLOCKER comments)`);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
const blocks = data.values.flatMap((comment) => formatCommentThread(comment));
|
|
308
|
+
sections.push(`Comments (${data.values.length} BLOCKER thread(s))${pageHint(data)}:\n\n${blocks.join('\n\n')}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const activityData = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/activities?limit=${commentsLimit}&start=${commentsStart}`);
|
|
313
|
+
const comments = uniqueCommentsFromActivities(activityData?.values ?? []).filter((comment) => {
|
|
314
|
+
const matchesState = commentMatchesState(comment, commentsState);
|
|
315
|
+
return matchesState && commentMatchesSeverity(comment, commentsSeverity);
|
|
316
|
+
});
|
|
317
|
+
if (comments.length === 0) {
|
|
318
|
+
sections.push('Comments:\n(no matching comments)');
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
const blocks = comments.flatMap((comment) => formatCommentThread(comment));
|
|
322
|
+
const paging = activityData ? pageHint(activityData) : '';
|
|
323
|
+
sections.push(`Comments (${comments.length} thread(s))${paging}:\n\n${blocks.join('\n\n')}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (includeDiff) {
|
|
328
|
+
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/diff`);
|
|
329
|
+
sections.push(`Diff:\n${data ? formatDiff(data, args.diffMaxChars ?? 8000) : '(no diff found)'}`);
|
|
330
|
+
}
|
|
331
|
+
return text(sections.join('\n\n'));
|
|
332
|
+
}
|
|
186
333
|
async getPrDiff(args) {
|
|
187
334
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
188
335
|
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/diff`);
|
|
@@ -193,7 +340,8 @@ export class BitbucketClient {
|
|
|
193
340
|
async getPrCommits(args) {
|
|
194
341
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
195
342
|
const limit = args.limit ?? 25;
|
|
196
|
-
const
|
|
343
|
+
const start = args.start ?? 0;
|
|
344
|
+
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/commits?limit=${limit}&start=${start}`);
|
|
197
345
|
if (!data || data.values.length === 0)
|
|
198
346
|
return text('No commits found.');
|
|
199
347
|
const lines = data.values.map((c) => `${c.displayId} ${formatDate(c.authorTimestamp)} ${c.author.name}: ${c.message.split('\n')[0]}`);
|
|
@@ -201,11 +349,15 @@ export class BitbucketClient {
|
|
|
201
349
|
}
|
|
202
350
|
async createPullRequest(args) {
|
|
203
351
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
204
|
-
const
|
|
352
|
+
const sourceBranch = args.fromBranch ?? safeExec('git rev-parse --abbrev-ref HEAD');
|
|
353
|
+
if (!sourceBranch || sourceBranch === 'HEAD') {
|
|
354
|
+
throw new Error('Could not determine source branch. Provide fromBranch or run from a checked-out branch.');
|
|
355
|
+
}
|
|
356
|
+
const { title, description, toBranch = 'master', reviewers = [] } = args;
|
|
205
357
|
const body = {
|
|
206
358
|
title,
|
|
207
359
|
description: description ?? '',
|
|
208
|
-
fromRef: { id: toBranchRef(
|
|
360
|
+
fromRef: { id: toBranchRef(sourceBranch), repository: { slug: repoSlug, project: { key: projectKey } } },
|
|
209
361
|
toRef: { id: toBranchRef(toBranch), repository: { slug: repoSlug, project: { key: projectKey } } },
|
|
210
362
|
reviewers: reviewers.map((name) => ({ user: { name } })),
|
|
211
363
|
};
|
|
@@ -279,15 +431,82 @@ export class BitbucketClient {
|
|
|
279
431
|
}
|
|
280
432
|
async getPrComments(args) {
|
|
281
433
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
287
|
-
|
|
434
|
+
const limit = args.limit ?? 50;
|
|
435
|
+
const start = args.start ?? 0;
|
|
436
|
+
const severity = args.severity ?? 'ALL';
|
|
437
|
+
const state = args.state ?? (args.countOnly ? undefined : 'OPEN');
|
|
438
|
+
if (args.countOnly) {
|
|
439
|
+
if (severity !== 'BLOCKER') {
|
|
440
|
+
throw new Error('countOnly is supported only for BLOCKER severity. Set severity="BLOCKER".');
|
|
441
|
+
}
|
|
442
|
+
if (state === 'PENDING') {
|
|
443
|
+
throw new Error('PENDING is not valid for blocker comment counts. Use OPEN or RESOLVED.');
|
|
444
|
+
}
|
|
445
|
+
const qs = new URLSearchParams({ count: 'true' });
|
|
446
|
+
if (state)
|
|
447
|
+
qs.set('state', state);
|
|
448
|
+
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/blocker-comments?${qs}`);
|
|
449
|
+
let open = data?.open ?? 0;
|
|
450
|
+
let resolved = data?.resolved ?? 0;
|
|
451
|
+
if ((open === 0 && resolved === 0) && data?.values && data.values.length > 0) {
|
|
452
|
+
for (const v of data.values) {
|
|
453
|
+
if ((v.state ?? '').toUpperCase() === 'OPEN')
|
|
454
|
+
open = v.count ?? open;
|
|
455
|
+
if ((v.state ?? '').toUpperCase() === 'RESOLVED')
|
|
456
|
+
resolved = v.count ?? resolved;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (state === 'OPEN')
|
|
460
|
+
return text(`PR #${args.prId} BLOCKER comments: OPEN=${open}`);
|
|
461
|
+
if (state === 'RESOLVED')
|
|
462
|
+
return text(`PR #${args.prId} BLOCKER comments: RESOLVED=${resolved}`);
|
|
463
|
+
return text(`PR #${args.prId} BLOCKER comments: OPEN=${open}, RESOLVED=${resolved}`);
|
|
464
|
+
}
|
|
465
|
+
if (severity === 'BLOCKER' && !args.path) {
|
|
466
|
+
if (state === 'PENDING') {
|
|
467
|
+
throw new Error('PENDING is not valid for blocker comments. Use OPEN or RESOLVED.');
|
|
468
|
+
}
|
|
469
|
+
const qs = new URLSearchParams({ limit: String(limit), start: String(start) });
|
|
470
|
+
if (state)
|
|
471
|
+
qs.set('state', state);
|
|
472
|
+
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/blocker-comments?${qs}`);
|
|
473
|
+
if (!data || data.values.length === 0) {
|
|
474
|
+
return text(`No ${state ?? 'OPEN/RESOLVED'} BLOCKER comments on PR #${args.prId}.`);
|
|
475
|
+
}
|
|
476
|
+
const blocks = data.values.flatMap((comment) => formatCommentThread(comment));
|
|
477
|
+
return text(`${data.values.length} ${state ?? 'OPEN/RESOLVED'} BLOCKER comment thread(s) on PR #${args.prId}${pageHint(data)}:\n\n${blocks.join('\n\n')}`);
|
|
478
|
+
}
|
|
479
|
+
if (args.path) {
|
|
480
|
+
const qs = new URLSearchParams({
|
|
481
|
+
limit: String(limit),
|
|
482
|
+
start: String(start),
|
|
483
|
+
path: args.path,
|
|
484
|
+
});
|
|
485
|
+
if (state)
|
|
486
|
+
qs.set('state', state);
|
|
487
|
+
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments?${qs}`);
|
|
488
|
+
const filtered = (data?.values ?? []).filter((comment) => {
|
|
489
|
+
const matchesState = state ? commentMatchesState(comment, state) : true;
|
|
490
|
+
return matchesState && commentMatchesSeverity(comment, severity);
|
|
491
|
+
});
|
|
492
|
+
if (filtered.length === 0) {
|
|
493
|
+
return text(`No matching comments on PR #${args.prId} for path ${args.path}.`);
|
|
494
|
+
}
|
|
495
|
+
const blocks = filtered.flatMap((comment) => formatCommentThread(comment));
|
|
496
|
+
const paging = data ? pageHint(data) : '';
|
|
497
|
+
return text(`${filtered.length} comment thread(s) on PR #${args.prId} for ${args.path}${paging}:\n\n${blocks.join('\n\n')}`);
|
|
498
|
+
}
|
|
499
|
+
const activityData = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/activities?limit=${limit}&start=${start}`);
|
|
500
|
+
const comments = uniqueCommentsFromActivities(activityData?.values ?? []).filter((comment) => {
|
|
501
|
+
const matchesState = state ? commentMatchesState(comment, state) : true;
|
|
502
|
+
return matchesState && commentMatchesSeverity(comment, severity);
|
|
503
|
+
});
|
|
504
|
+
if (comments.length === 0) {
|
|
505
|
+
return text(`No matching comments on PR #${args.prId}.`);
|
|
288
506
|
}
|
|
289
|
-
const blocks =
|
|
290
|
-
|
|
507
|
+
const blocks = comments.flatMap((comment) => formatCommentThread(comment));
|
|
508
|
+
const paging = activityData ? pageHint(activityData) : '';
|
|
509
|
+
return text(`${comments.length} comment thread(s) on PR #${args.prId}${paging}:\n\n${blocks.join('\n\n')}`);
|
|
291
510
|
}
|
|
292
511
|
async addPrComment(args) {
|
|
293
512
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
package/dist/context.js
CHANGED
|
@@ -70,29 +70,3 @@ export async function getDevContext(args, jira, bitbucket) {
|
|
|
70
70
|
}
|
|
71
71
|
return { content: [{ type: 'text', text: sections.join('\n\n') }] };
|
|
72
72
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Creates a Bitbucket PR using the current git repo to auto-detect project, repo, and branch.
|
|
75
|
-
*/
|
|
76
|
-
export async function createPrFromContext(args, bitbucket) {
|
|
77
|
-
const repoPath = args.repoPath ?? process.cwd();
|
|
78
|
-
const remote = safeExec('git remote get-url origin', repoPath);
|
|
79
|
-
if (!remote)
|
|
80
|
-
throw new Error('No git remote found — are you in a git repository?');
|
|
81
|
-
const parsed = parseBitbucketRemote(remote);
|
|
82
|
-
if (!parsed)
|
|
83
|
-
throw new Error(`Could not parse Bitbucket project/repo from remote: ${remote}`);
|
|
84
|
-
const branch = safeExec('git rev-parse --abbrev-ref HEAD', repoPath);
|
|
85
|
-
if (!branch)
|
|
86
|
-
throw new Error('Could not determine current branch.');
|
|
87
|
-
if (branch === 'HEAD')
|
|
88
|
-
throw new Error('Detached HEAD state — check out a branch first.');
|
|
89
|
-
return bitbucket.createPullRequest({
|
|
90
|
-
projectKey: parsed.projectKey,
|
|
91
|
-
repoSlug: parsed.repoSlug,
|
|
92
|
-
title: args.title,
|
|
93
|
-
description: args.description,
|
|
94
|
-
fromBranch: branch,
|
|
95
|
-
toBranch: args.toBranch,
|
|
96
|
-
reviewers: args.reviewers,
|
|
97
|
-
});
|
|
98
|
-
}
|