@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 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 37 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,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
- | `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, 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 with IDs/states (optional `path` filter) |
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 (`NORMAL` / `BLOCKER`) |
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. 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.
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
- throw new Error(`Bitbucket ${res.status} ${method} ${path}: ${errText}`);
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
- throw new Error(`Bitbucket ${res.status} GET ${path}: ${errText}`);
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 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}`;
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 { 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;
224
357
  const body = {
225
358
  title,
226
359
  description: description ?? '',
227
- fromRef: { id: toBranchRef(fromBranch), repository: { slug: repoSlug, project: { key: projectKey } } },
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 { limit = 50, start = 0, state = 'OPEN' } = args;
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) => commentMatchesState(comment, state));
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 ${state} comments on PR #${args.prId} for path ${args.path}.`);
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) => commentMatchesState(comment, state));
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 ${state} comments on PR #${args.prId}.`);
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
- }