@stubbedev/atlassian-mcp 0.1.7 → 0.1.9
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/LICENSE +21 -0
- package/README.md +6 -2
- package/dist/bitbucket.js +123 -2
- package/dist/index.js +58 -1
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stubbedev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -45,7 +45,9 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
45
45
|
| `bitbucket_get_pull_request` | Get pull request details |
|
|
46
46
|
| `bitbucket_get_pr_overview` | Get a one-call PR overview: metadata, commits, comments, task-style BLOCKER comments, and optional diff |
|
|
47
47
|
| `bitbucket_get_pr_diff` | Get the code diff for a pull request |
|
|
48
|
-
| `bitbucket_create_pull_request` | Create a new pull request |
|
|
48
|
+
| `bitbucket_create_pull_request` | Create a new pull request (checks for an existing open PR from the source branch first) |
|
|
49
|
+
| `bitbucket_update_pull_request` | Update pull request title, description, destination branch, or reviewers |
|
|
50
|
+
| `bitbucket_mutate_pull_request` | Create or update a pull request in one call (target by PR ID or source branch) |
|
|
49
51
|
| `bitbucket_approve_pr` | Approve a pull request |
|
|
50
52
|
| `bitbucket_unapprove_pr` | Remove your approval from a pull request |
|
|
51
53
|
| `bitbucket_merge_pr` | Merge a pull request |
|
|
@@ -73,6 +75,8 @@ All list tools support `limit` and `start`/`startAt` for pagination.
|
|
|
73
75
|
- "show my PRs waiting for review" → `bitbucket_my_prs`
|
|
74
76
|
- "list open PRs for this repo from branch feature/ABC-123" → `bitbucket_list_pull_requests`
|
|
75
77
|
- "open a PR from my current branch to master" → `bitbucket_create_pull_request`
|
|
78
|
+
- "update PR 42 title and reviewers" → `bitbucket_update_pull_request`
|
|
79
|
+
- "create or update PR from this branch in one call" → `bitbucket_mutate_pull_request`
|
|
76
80
|
- "show review comments on PR 42" → `bitbucket_get_pr_comments`
|
|
77
81
|
- "give me one full overview of PR 42" → `bitbucket_get_pr_overview`
|
|
78
82
|
- "how many open blockers are on PR 42" → `bitbucket_get_pr_comments` with `severity=BLOCKER` and `countOnly=true`
|
|
@@ -113,7 +117,7 @@ The `$schema` field is optional but enables editor autocomplete and validation.
|
|
|
113
117
|
- Jira: `project` (alias of `projectKey`)
|
|
114
118
|
- Bitbucket: `project` and `repo` (aliases of `projectKey` and `repoSlug`)
|
|
115
119
|
- For Bitbucket tools, `projectKey` and `repoSlug` are usually auto-detected from your local `origin` remote.
|
|
116
|
-
- `bitbucket_create_pull_request` also auto-detects `fromBranch` from your current branch.
|
|
120
|
+
- `bitbucket_create_pull_request` also auto-detects `fromBranch` from your current branch and returns the existing open PR if one already exists for that branch.
|
|
117
121
|
- Jira project-scoped calls accept `projectKey` and work best when provided.
|
|
118
122
|
- 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.
|
|
119
123
|
|
package/dist/bitbucket.js
CHANGED
|
@@ -31,6 +31,9 @@ function text(t) {
|
|
|
31
31
|
function toBranchRef(branch) {
|
|
32
32
|
return branch.startsWith('refs/') ? branch : `refs/heads/${branch}`;
|
|
33
33
|
}
|
|
34
|
+
function branchDisplayId(branch) {
|
|
35
|
+
return branch.replace(/^refs\/heads\//, '');
|
|
36
|
+
}
|
|
34
37
|
function formatDate(ms) {
|
|
35
38
|
return new Date(ms).toISOString().slice(0, 10);
|
|
36
39
|
}
|
|
@@ -236,8 +239,17 @@ export class BitbucketClient {
|
|
|
236
239
|
}
|
|
237
240
|
// Used internally by context tools — finds the open PR for a given source branch
|
|
238
241
|
async findOpenPrForBranch(projectKey, repoSlug, branch) {
|
|
239
|
-
const
|
|
240
|
-
|
|
242
|
+
const targetBranch = branchDisplayId(branch);
|
|
243
|
+
let start = 0;
|
|
244
|
+
while (true) {
|
|
245
|
+
const data = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests?state=OPEN&limit=50&start=${start}`);
|
|
246
|
+
const match = data?.values.find((pr) => branchDisplayId(pr.fromRef.displayId) === targetBranch) ?? null;
|
|
247
|
+
if (match)
|
|
248
|
+
return match;
|
|
249
|
+
if (!data || data.isLastPage || data.nextPageStart === undefined)
|
|
250
|
+
return null;
|
|
251
|
+
start = data.nextPageStart;
|
|
252
|
+
}
|
|
241
253
|
}
|
|
242
254
|
async listRepos(args) {
|
|
243
255
|
const { limit = 50, start = 0 } = args;
|
|
@@ -399,6 +411,12 @@ export class BitbucketClient {
|
|
|
399
411
|
if (!sourceBranch || sourceBranch === 'HEAD') {
|
|
400
412
|
throw new Error('Could not determine source branch. Provide fromBranch or run from a checked-out branch.');
|
|
401
413
|
}
|
|
414
|
+
const sourceBranchName = branchDisplayId(sourceBranch);
|
|
415
|
+
const existing = await this.findOpenPrForBranch(projectKey, repoSlug, sourceBranchName);
|
|
416
|
+
if (existing) {
|
|
417
|
+
const url = this.pullRequestUrl(projectKey, repoSlug, existing.id, existing);
|
|
418
|
+
return text(`Open PR already exists for branch "${sourceBranchName}": #${existing.id} "${existing.title}"\n${url}`);
|
|
419
|
+
}
|
|
402
420
|
const { title, description, toBranch = 'master', reviewers = [] } = args;
|
|
403
421
|
const body = {
|
|
404
422
|
title,
|
|
@@ -413,6 +431,109 @@ export class BitbucketClient {
|
|
|
413
431
|
const url = this.pullRequestUrl(projectKey, repoSlug, data.id, data);
|
|
414
432
|
return text(`Created PR #${data.id}: "${data.title}"\n${url}`);
|
|
415
433
|
}
|
|
434
|
+
async updatePullRequest(args) {
|
|
435
|
+
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
436
|
+
if (args.title === undefined
|
|
437
|
+
&& args.description === undefined
|
|
438
|
+
&& args.toBranch === undefined
|
|
439
|
+
&& args.reviewers === undefined) {
|
|
440
|
+
throw new Error('At least one field is required: title, description, toBranch, or reviewers');
|
|
441
|
+
}
|
|
442
|
+
const existing = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}`);
|
|
443
|
+
if (!existing)
|
|
444
|
+
throw new Error(`PR #${args.prId} not found.`);
|
|
445
|
+
const buildBody = (version) => {
|
|
446
|
+
const body = { version };
|
|
447
|
+
if (args.title !== undefined)
|
|
448
|
+
body.title = args.title;
|
|
449
|
+
if (args.description !== undefined)
|
|
450
|
+
body.description = args.description;
|
|
451
|
+
if (args.toBranch !== undefined) {
|
|
452
|
+
body.toRef = {
|
|
453
|
+
id: toBranchRef(args.toBranch),
|
|
454
|
+
repository: { slug: repoSlug, project: { key: projectKey } },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (args.reviewers !== undefined) {
|
|
458
|
+
body.reviewers = args.reviewers.map((name) => ({ user: { name } }));
|
|
459
|
+
}
|
|
460
|
+
return body;
|
|
461
|
+
};
|
|
462
|
+
let updated;
|
|
463
|
+
try {
|
|
464
|
+
updated = await this.request('PUT', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}`, buildBody(existing.version));
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
468
|
+
if (!message.includes('Bitbucket 409'))
|
|
469
|
+
throw error;
|
|
470
|
+
const latest = await this.request('GET', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}`);
|
|
471
|
+
if (!latest)
|
|
472
|
+
throw error;
|
|
473
|
+
updated = await this.request('PUT', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}`, buildBody(latest.version));
|
|
474
|
+
}
|
|
475
|
+
if (!updated)
|
|
476
|
+
return text(`Updated PR #${args.prId}.`);
|
|
477
|
+
const url = this.pullRequestUrl(projectKey, repoSlug, updated.id, updated);
|
|
478
|
+
return text(`Updated PR #${updated.id}: "${updated.title}" (${updated.fromRef.displayId} → ${updated.toRef.displayId}).\n${url}`);
|
|
479
|
+
}
|
|
480
|
+
async mutatePullRequest(args) {
|
|
481
|
+
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
482
|
+
const hasUpdate = args.update !== undefined && (args.update.title !== undefined
|
|
483
|
+
|| args.update.description !== undefined
|
|
484
|
+
|| args.update.toBranch !== undefined
|
|
485
|
+
|| args.update.reviewers !== undefined);
|
|
486
|
+
if (args.prId !== undefined) {
|
|
487
|
+
if (!hasUpdate) {
|
|
488
|
+
return this.getPullRequest({ projectKey, repoSlug, prId: args.prId });
|
|
489
|
+
}
|
|
490
|
+
return this.updatePullRequest({
|
|
491
|
+
projectKey,
|
|
492
|
+
repoSlug,
|
|
493
|
+
prId: args.prId,
|
|
494
|
+
...args.update,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
const sourceBranch = args.create?.fromBranch ?? safeExec('git rev-parse --abbrev-ref HEAD');
|
|
498
|
+
if (!sourceBranch || sourceBranch === 'HEAD') {
|
|
499
|
+
if (args.create) {
|
|
500
|
+
return this.createPullRequest({
|
|
501
|
+
projectKey,
|
|
502
|
+
repoSlug,
|
|
503
|
+
title: args.create.title,
|
|
504
|
+
description: args.create.description,
|
|
505
|
+
fromBranch: args.create.fromBranch,
|
|
506
|
+
toBranch: args.create.toBranch,
|
|
507
|
+
reviewers: args.create.reviewers,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
throw new Error('Could not determine source branch. Provide create.fromBranch or run from a checked-out branch.');
|
|
511
|
+
}
|
|
512
|
+
const existing = await this.findOpenPrForBranch(projectKey, repoSlug, sourceBranch);
|
|
513
|
+
if (existing) {
|
|
514
|
+
if (hasUpdate) {
|
|
515
|
+
return this.updatePullRequest({
|
|
516
|
+
projectKey,
|
|
517
|
+
repoSlug,
|
|
518
|
+
prId: existing.id,
|
|
519
|
+
...args.update,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return this.getPullRequest({ projectKey, repoSlug, prId: existing.id });
|
|
523
|
+
}
|
|
524
|
+
if (!args.create) {
|
|
525
|
+
throw new Error(`No open PR found for branch "${branchDisplayId(sourceBranch)}". Provide create to open one.`);
|
|
526
|
+
}
|
|
527
|
+
return this.createPullRequest({
|
|
528
|
+
projectKey,
|
|
529
|
+
repoSlug,
|
|
530
|
+
title: args.create.title,
|
|
531
|
+
description: args.create.description,
|
|
532
|
+
fromBranch: args.create.fromBranch,
|
|
533
|
+
toBranch: args.create.toBranch,
|
|
534
|
+
reviewers: args.create.reviewers,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
416
537
|
async approvePr(args) {
|
|
417
538
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
418
539
|
const data = await this.request('POST', `/projects/${projectKey}/repos/${repoSlug}/pull-requests/${args.prId}/approve`);
|
package/dist/index.js
CHANGED
|
@@ -413,7 +413,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
413
413
|
},
|
|
414
414
|
{
|
|
415
415
|
name: 'bitbucket_create_pull_request',
|
|
416
|
-
description: 'Use when you want to open a new PR. Project/repo auto-detect from git remote, source branch auto-detects from current branch if omitted, and
|
|
416
|
+
description: 'Use when you want to open a new PR. Project/repo auto-detect from git remote, source branch auto-detects from current branch if omitted, and this call first checks for an existing open PR from that source branch.',
|
|
417
417
|
inputSchema: {
|
|
418
418
|
type: 'object',
|
|
419
419
|
properties: {
|
|
@@ -430,6 +430,59 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
430
430
|
required: ['title'],
|
|
431
431
|
},
|
|
432
432
|
},
|
|
433
|
+
{
|
|
434
|
+
name: 'bitbucket_update_pull_request',
|
|
435
|
+
description: 'Use when you want to update an existing PR title, description, destination branch, or reviewers. You can pass projectKey/repoSlug or project/repo.',
|
|
436
|
+
inputSchema: {
|
|
437
|
+
type: 'object',
|
|
438
|
+
properties: {
|
|
439
|
+
projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
|
|
440
|
+
project: { type: 'string', description: 'Alias for projectKey' },
|
|
441
|
+
repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
|
|
442
|
+
repo: { type: 'string', description: 'Alias for repoSlug' },
|
|
443
|
+
prId: { type: 'number', description: 'Pull request number (PR ID)' },
|
|
444
|
+
title: { type: 'string', description: 'Updated PR title (optional)' },
|
|
445
|
+
description: { type: 'string', description: 'Updated PR description, or empty string to clear (optional)' },
|
|
446
|
+
toBranch: { type: 'string', description: 'Updated target branch name (optional)' },
|
|
447
|
+
reviewers: { type: 'array', items: { type: 'string' }, description: 'Updated reviewer usernames (optional). Pass an empty array to clear reviewers.' },
|
|
448
|
+
},
|
|
449
|
+
required: ['prId'],
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: 'bitbucket_mutate_pull_request',
|
|
454
|
+
description: 'Use when you want one ergonomic PR mutation call: target a PR by prId, or auto-target the open PR from create.fromBranch/current branch, then optionally update it; if none exists and create is provided, create a new PR.',
|
|
455
|
+
inputSchema: {
|
|
456
|
+
type: 'object',
|
|
457
|
+
properties: {
|
|
458
|
+
projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG" (usually auto-detected)' },
|
|
459
|
+
project: { type: 'string', description: 'Alias for projectKey' },
|
|
460
|
+
repoSlug: { type: 'string', description: 'Repository slug, e.g. "payments-service" (usually auto-detected)' },
|
|
461
|
+
repo: { type: 'string', description: 'Alias for repoSlug' },
|
|
462
|
+
prId: { type: 'number', description: 'Target pull request number (optional). If omitted, tool targets the open PR for create.fromBranch/current branch.' },
|
|
463
|
+
create: {
|
|
464
|
+
type: 'object',
|
|
465
|
+
properties: {
|
|
466
|
+
title: { type: 'string', description: 'PR title' },
|
|
467
|
+
description: { type: 'string', description: 'PR description (optional)' },
|
|
468
|
+
fromBranch: { type: 'string', description: 'Source branch to target/create from (defaults to current branch)' },
|
|
469
|
+
toBranch: { type: 'string', description: 'Destination branch (default: master when creating)' },
|
|
470
|
+
reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer usernames (optional)' },
|
|
471
|
+
},
|
|
472
|
+
required: ['title'],
|
|
473
|
+
},
|
|
474
|
+
update: {
|
|
475
|
+
type: 'object',
|
|
476
|
+
properties: {
|
|
477
|
+
title: { type: 'string', description: 'Updated PR title (optional)' },
|
|
478
|
+
description: { type: 'string', description: 'Updated PR description, or empty string to clear (optional)' },
|
|
479
|
+
toBranch: { type: 'string', description: 'Updated target branch (optional)' },
|
|
480
|
+
reviewers: { type: 'array', items: { type: 'string' }, description: 'Updated reviewer usernames (optional). Pass an empty array to clear reviewers.' },
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
433
486
|
{
|
|
434
487
|
name: 'bitbucket_approve_pr',
|
|
435
488
|
description: 'Use when you want to approve a PR. You can pass projectKey/repoSlug or project/repo.',
|
|
@@ -694,6 +747,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
694
747
|
return await bitbucket.getPrCommits(normalizeBitbucketArgs(args));
|
|
695
748
|
case 'bitbucket_create_pull_request':
|
|
696
749
|
return await bitbucket.createPullRequest(normalizeBitbucketArgs(args));
|
|
750
|
+
case 'bitbucket_update_pull_request':
|
|
751
|
+
return await bitbucket.updatePullRequest(normalizeBitbucketArgs(args));
|
|
752
|
+
case 'bitbucket_mutate_pull_request':
|
|
753
|
+
return await bitbucket.mutatePullRequest(normalizeBitbucketArgs(args));
|
|
697
754
|
case 'bitbucket_approve_pr':
|
|
698
755
|
return await bitbucket.approvePr(normalizeBitbucketArgs(args));
|
|
699
756
|
case 'bitbucket_unapprove_pr':
|