@stubbedev/atlassian-mcp 0.0.5 → 0.1.1
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 +37 -12
- package/dist/bitbucket.js +196 -38
- package/dist/context.js +0 -26
- package/dist/index.js +193 -176
- 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,29 +28,26 @@ 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, task-style BLOCKER comments, 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 PR comment threads
|
|
50
|
-
| `bitbucket_get_pr_tasks` | List PR tasks (blocker comments), with `OPEN`/`RESOLVED` filter |
|
|
51
|
-
| `bitbucket_get_pr_task_count` | Get total OPEN and RESOLVED PR task counts |
|
|
48
|
+
| `bitbucket_get_pr_comments` | Get PR comment threads in bulk, including task-style BLOCKER comments and blocker counts |
|
|
52
49
|
| `bitbucket_add_pr_comment` | Add a top-level PR comment or reply to an existing comment |
|
|
53
|
-
| `bitbucket_update_pr_comment` | Update comment text, state, or severity (`
|
|
50
|
+
| `bitbucket_update_pr_comment` | Update comment text, state, or severity (`BLOCKER` = task/checklist item) |
|
|
54
51
|
| `bitbucket_delete_pr_comment` | Delete a PR comment by comment ID |
|
|
55
52
|
| `bitbucket_get_pr_commits` | List commits included in a pull request |
|
|
56
53
|
| `bitbucket_get_branches` | List branches in a repository |
|
|
@@ -66,6 +63,18 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
66
63
|
|
|
67
64
|
All list tools support `limit` and `start`/`startAt` for pagination.
|
|
68
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
|
+
|
|
69
78
|
---
|
|
70
79
|
|
|
71
80
|
## Setup
|
|
@@ -88,7 +97,18 @@ Create `~/.atlassian-mcp.json`:
|
|
|
88
97
|
}
|
|
89
98
|
```
|
|
90
99
|
|
|
91
|
-
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.
|
|
92
112
|
|
|
93
113
|
Alternatively, use environment variables (or a `.env` file in this directory):
|
|
94
114
|
|
|
@@ -239,6 +259,8 @@ Then use `node /path/to/atlassian-mcp/dist/index.js` instead of the `npx` comman
|
|
|
239
259
|
|
|
240
260
|
This package is published to npm as `@stubbedev/atlassian-mcp`.
|
|
241
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
|
+
|
|
242
264
|
Automatic publish is configured in `.github/workflows/publish.yml`:
|
|
243
265
|
|
|
244
266
|
- Push a tag like `v1.0.1` to publish from CI
|
|
@@ -306,6 +328,9 @@ node dist/index.js
|
|
|
306
328
|
|
|
307
329
|
# Test the tool list
|
|
308
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
|
|
309
334
|
```
|
|
310
335
|
|
|
311
336
|
To use a specific config file:
|
package/dist/bitbucket.js
CHANGED
|
@@ -55,6 +55,14 @@ function commentMatchesState(comment, state) {
|
|
|
55
55
|
return true;
|
|
56
56
|
return (comment.comments ?? []).some((child) => commentMatchesState(child, state));
|
|
57
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
|
+
}
|
|
58
66
|
function uniqueCommentsFromActivities(activities) {
|
|
59
67
|
const byId = new Map();
|
|
60
68
|
for (const activity of activities) {
|
|
@@ -90,6 +98,44 @@ function formatDiff(data, maxChars = 8000) {
|
|
|
90
98
|
}
|
|
91
99
|
return result;
|
|
92
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
|
+
}
|
|
93
139
|
export class BitbucketClient {
|
|
94
140
|
baseUrl;
|
|
95
141
|
headers;
|
|
@@ -124,7 +170,8 @@ export class BitbucketClient {
|
|
|
124
170
|
const res = await fetch(url, opts);
|
|
125
171
|
if (!res.ok) {
|
|
126
172
|
const errText = await res.text();
|
|
127
|
-
|
|
173
|
+
const details = parseBitbucketErrorDetails(errText);
|
|
174
|
+
throw new Error(formatBitbucketError(res.status, method, path, details));
|
|
128
175
|
}
|
|
129
176
|
return res.status === 204 ? null : res.json();
|
|
130
177
|
}
|
|
@@ -136,7 +183,8 @@ export class BitbucketClient {
|
|
|
136
183
|
});
|
|
137
184
|
if (!res.ok) {
|
|
138
185
|
const errText = await res.text();
|
|
139
|
-
|
|
186
|
+
const details = parseBitbucketErrorDetails(errText);
|
|
187
|
+
throw new Error(formatBitbucketError(res.status, 'GET', path, details));
|
|
140
188
|
}
|
|
141
189
|
return res.text();
|
|
142
190
|
}
|
|
@@ -159,8 +207,13 @@ export class BitbucketClient {
|
|
|
159
207
|
}
|
|
160
208
|
async listPullRequests(args) {
|
|
161
209
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
162
|
-
const { state = 'OPEN', limit = 25, start = 0 } = args;
|
|
163
|
-
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}`;
|
|
164
217
|
const data = await this.request('GET', path);
|
|
165
218
|
if (!data || data.values.length === 0)
|
|
166
219
|
return text(`No ${state} pull requests found.`);
|
|
@@ -201,6 +254,82 @@ export class BitbucketClient {
|
|
|
201
254
|
];
|
|
202
255
|
return text(lines.join('\n'));
|
|
203
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
|
+
}
|
|
204
333
|
async getPrDiff(args) {
|
|
205
334
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
206
335
|
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/diff`);
|
|
@@ -220,11 +349,15 @@ export class BitbucketClient {
|
|
|
220
349
|
}
|
|
221
350
|
async createPullRequest(args) {
|
|
222
351
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
223
|
-
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;
|
|
224
357
|
const body = {
|
|
225
358
|
title,
|
|
226
359
|
description: description ?? '',
|
|
227
|
-
fromRef: { id: toBranchRef(
|
|
360
|
+
fromRef: { id: toBranchRef(sourceBranch), repository: { slug: repoSlug, project: { key: projectKey } } },
|
|
228
361
|
toRef: { id: toBranchRef(toBranch), repository: { slug: repoSlug, project: { key: projectKey } } },
|
|
229
362
|
reviewers: reviewers.map((name) => ({ user: { name } })),
|
|
230
363
|
};
|
|
@@ -298,58 +431,83 @@ export class BitbucketClient {
|
|
|
298
431
|
}
|
|
299
432
|
async getPrComments(args) {
|
|
300
433
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
301
|
-
const
|
|
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
|
+
}
|
|
302
479
|
if (args.path) {
|
|
303
480
|
const qs = new URLSearchParams({
|
|
304
481
|
limit: String(limit),
|
|
305
482
|
start: String(start),
|
|
306
|
-
state,
|
|
307
483
|
path: args.path,
|
|
308
484
|
});
|
|
485
|
+
if (state)
|
|
486
|
+
qs.set('state', state);
|
|
309
487
|
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments?${qs}`);
|
|
310
|
-
const filtered = (data?.values ?? []).filter((comment) =>
|
|
488
|
+
const filtered = (data?.values ?? []).filter((comment) => {
|
|
489
|
+
const matchesState = state ? commentMatchesState(comment, state) : true;
|
|
490
|
+
return matchesState && commentMatchesSeverity(comment, severity);
|
|
491
|
+
});
|
|
311
492
|
if (filtered.length === 0) {
|
|
312
|
-
return text(`No
|
|
493
|
+
return text(`No matching comments on PR #${args.prId} for path ${args.path}.`);
|
|
313
494
|
}
|
|
314
495
|
const blocks = filtered.flatMap((comment) => formatCommentThread(comment));
|
|
315
496
|
const paging = data ? pageHint(data) : '';
|
|
316
497
|
return text(`${filtered.length} comment thread(s) on PR #${args.prId} for ${args.path}${paging}:\n\n${blocks.join('\n\n')}`);
|
|
317
498
|
}
|
|
318
499
|
const activityData = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/activities?limit=${limit}&start=${start}`);
|
|
319
|
-
const comments = uniqueCommentsFromActivities(activityData?.values ?? []).filter((comment) =>
|
|
500
|
+
const comments = uniqueCommentsFromActivities(activityData?.values ?? []).filter((comment) => {
|
|
501
|
+
const matchesState = state ? commentMatchesState(comment, state) : true;
|
|
502
|
+
return matchesState && commentMatchesSeverity(comment, severity);
|
|
503
|
+
});
|
|
320
504
|
if (comments.length === 0) {
|
|
321
|
-
return text(`No
|
|
505
|
+
return text(`No matching comments on PR #${args.prId}.`);
|
|
322
506
|
}
|
|
323
507
|
const blocks = comments.flatMap((comment) => formatCommentThread(comment));
|
|
324
508
|
const paging = activityData ? pageHint(activityData) : '';
|
|
325
509
|
return text(`${comments.length} comment thread(s) on PR #${args.prId}${paging}:\n\n${blocks.join('\n\n')}`);
|
|
326
510
|
}
|
|
327
|
-
async getPrTasks(args) {
|
|
328
|
-
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
329
|
-
const { limit = 50, start = 0, state = 'OPEN' } = args;
|
|
330
|
-
const qs = new URLSearchParams({ limit: String(limit), start: String(start), state });
|
|
331
|
-
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/blocker-comments?${qs}`);
|
|
332
|
-
if (!data || data.values.length === 0) {
|
|
333
|
-
return text(`No ${state} tasks on PR #${args.prId}.`);
|
|
334
|
-
}
|
|
335
|
-
const blocks = data.values.flatMap((comment) => formatCommentThread(comment));
|
|
336
|
-
return text(`${data.values.length} ${state} task(s) on PR #${args.prId}${pageHint(data)}:\n\n${blocks.join('\n\n')}`);
|
|
337
|
-
}
|
|
338
|
-
async getPrTaskCount(args) {
|
|
339
|
-
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
340
|
-
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/blocker-comments?count=true`);
|
|
341
|
-
let open = data?.open ?? 0;
|
|
342
|
-
let resolved = data?.resolved ?? 0;
|
|
343
|
-
if ((open === 0 && resolved === 0) && data?.values && data.values.length > 0) {
|
|
344
|
-
for (const v of data.values) {
|
|
345
|
-
if ((v.state ?? '').toUpperCase() === 'OPEN')
|
|
346
|
-
open = v.count ?? open;
|
|
347
|
-
if ((v.state ?? '').toUpperCase() === 'RESOLVED')
|
|
348
|
-
resolved = v.count ?? resolved;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return text(`PR #${args.prId} tasks: OPEN=${open}, RESOLVED=${resolved}`);
|
|
352
|
-
}
|
|
353
511
|
async addPrComment(args) {
|
|
354
512
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
355
513
|
const body = { text: args.text };
|
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
|
-
}
|