@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 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 34 tools to Claude for reading and managing issues, pull requests, comments, and git context.
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` | Unified snapshot: git state + linked Jira tickets (from branch name) + open PR for the current branch |
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` | Search issues by text, JQL, project, status, assignee, or issue type |
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
- | `jira_get_transitions` | List available status transitions |
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 for a repository |
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 with IDs and states for a pull request |
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. For Bitbucket tools, `projectKey` and `repoSlug` are auto-detected from your local `origin` remote when omitted. Jira project-specific tools still require `projectKey` in the tool call.
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
- throw new Error(`Bitbucket ${res.status} ${method} ${path}: ${errText}`);
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
- throw new Error(`Bitbucket ${res.status} GET ${path}: ${errText}`);
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 path = `/projects/${projectKey}/repos/${repoSlug}/pull-requests?state=${state}&limit=${limit}&start=${start}`;
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 data = await this.request('GET', `/inbox/pull-requests?limit=${limit}&start=${start}`);
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 data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/commits?limit=${limit}`);
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 { title, description, fromBranch, toBranch = 'master', reviewers = [] } = args;
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(fromBranch), repository: { slug: repoSlug, project: { key: projectKey } } },
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 { limit = 50, start = 0, state = 'OPEN' } = args;
283
- const qs = new URLSearchParams({ limit: String(limit), start: String(start) });
284
- qs.set('state', state);
285
- const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/comments?${qs}`);
286
- if (!data || data.values.length === 0) {
287
- return text(`No ${state} comments on PR #${args.prId}.`);
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 = data.values.flatMap((comment) => formatCommentThread(comment));
290
- return text(`${data.values.length} comment thread(s) on PR #${args.prId}${pageHint(data)}:\n\n${blocks.join('\n\n')}`);
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
- }