@stubbedev/atlassian-mcp 0.3.1 → 0.3.3
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/dist/bitbucket.js +120 -12
- package/dist/index.js +4 -2
- package/package.json +1 -1
package/dist/bitbucket.js
CHANGED
|
@@ -84,6 +84,7 @@ function commentMatchesState(comment, state) {
|
|
|
84
84
|
const threadState = comment.threadResolved ? 'RESOLVED' : 'OPEN';
|
|
85
85
|
if (threadState === state)
|
|
86
86
|
return true;
|
|
87
|
+
return (comment.comments ?? []).some((child) => commentMatchesState(child, state));
|
|
87
88
|
}
|
|
88
89
|
const currentState = comment.state ?? 'OPEN';
|
|
89
90
|
if (currentState === state)
|
|
@@ -125,18 +126,27 @@ function uniqueCommentsFromActivities(activities) {
|
|
|
125
126
|
}
|
|
126
127
|
return Array.from(byId.values())
|
|
127
128
|
.filter((comment) => !comment.deleted)
|
|
128
|
-
.sort((a, b) => (
|
|
129
|
+
.sort((a, b) => (b.createdDate ?? 0) - (a.createdDate ?? 0));
|
|
129
130
|
}
|
|
130
131
|
function pageHint(data) {
|
|
131
132
|
return data.isLastPage ? '' : ` (use start=${data.nextPageStart} for next page)`;
|
|
132
133
|
}
|
|
133
134
|
function formatDiff(data, maxChars = 8000) {
|
|
134
135
|
const parts = [];
|
|
136
|
+
if (data.fromHash && data.toHash) {
|
|
137
|
+
parts.push(`# fromHash=${data.fromHash} toHash=${data.toHash}`);
|
|
138
|
+
parts.push('# Pass these to bitbucket_comment as fromHash/toHash to anchor inline comments to this exact diff.');
|
|
139
|
+
}
|
|
135
140
|
for (const diff of data.diffs) {
|
|
136
141
|
const from = diff.source?.toString ?? '/dev/null';
|
|
137
142
|
const to = diff.destination?.toString ?? '/dev/null';
|
|
138
143
|
parts.push(`--- a/${from}\n+++ b/${to}`);
|
|
139
144
|
for (const hunk of diff.hunks ?? []) {
|
|
145
|
+
const srcLine = hunk.sourceLine ?? 0;
|
|
146
|
+
const srcSpan = hunk.sourceSpan ?? 0;
|
|
147
|
+
const dstLine = hunk.destinationLine ?? 0;
|
|
148
|
+
const dstSpan = hunk.destinationSpan ?? 0;
|
|
149
|
+
parts.push(`@@ -${srcLine},${srcSpan} +${dstLine},${dstSpan} @@`);
|
|
140
150
|
for (const segment of hunk.segments ?? []) {
|
|
141
151
|
const prefix = segment.type === 'ADDED' ? '+' : segment.type === 'REMOVED' ? '-' : ' ';
|
|
142
152
|
for (const line of segment.lines ?? []) {
|
|
@@ -326,6 +336,49 @@ export class BitbucketClient {
|
|
|
326
336
|
}
|
|
327
337
|
return res.status === 204 ? null : res.json();
|
|
328
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Remap a source-side line number through an interim diff.
|
|
341
|
+
*
|
|
342
|
+
* Returns the destination line if the source line survives unchanged through the diff
|
|
343
|
+
* (in a CONTEXT segment), or null if the line was modified/removed and cannot be remapped.
|
|
344
|
+
*/
|
|
345
|
+
async remapLineThroughDiff(projectKey, repoSlug, filePath, sinceHash, untilHash, sourceLine) {
|
|
346
|
+
const diff = await this.request('GET', `${this.rp(projectKey, repoSlug)}/diff/${filePath.split('/').map(encodeURIComponent).join('/')}?since=${encodeURIComponent(sinceHash)}&until=${encodeURIComponent(untilHash)}&contextLines=0`).catch(() => null);
|
|
347
|
+
if (!diff || !diff.diffs?.length)
|
|
348
|
+
return sourceLine;
|
|
349
|
+
let offset = 0;
|
|
350
|
+
for (const fileDiff of diff.diffs) {
|
|
351
|
+
for (const hunk of fileDiff.hunks ?? []) {
|
|
352
|
+
const srcStart = hunk.sourceLine ?? 0;
|
|
353
|
+
const srcSpan = hunk.sourceSpan ?? 0;
|
|
354
|
+
const srcEnd = srcStart + srcSpan - 1;
|
|
355
|
+
if (srcSpan > 0 && sourceLine >= srcStart && sourceLine <= srcEnd) {
|
|
356
|
+
for (const segment of hunk.segments ?? []) {
|
|
357
|
+
if (segment.type === 'ADDED')
|
|
358
|
+
continue;
|
|
359
|
+
for (const ln of segment.lines ?? []) {
|
|
360
|
+
if (ln.source === sourceLine) {
|
|
361
|
+
if (segment.type === 'CONTEXT' && ln.destination !== undefined)
|
|
362
|
+
return ln.destination;
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (sourceLine > srcEnd || srcSpan === 0) {
|
|
370
|
+
const dstSpan = hunk.destinationSpan ?? 0;
|
|
371
|
+
if (srcSpan === 0 && (hunk.destinationLine ?? 0) <= sourceLine + offset) {
|
|
372
|
+
offset += dstSpan;
|
|
373
|
+
}
|
|
374
|
+
else if (sourceLine > srcEnd) {
|
|
375
|
+
offset += dstSpan - srcSpan;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return sourceLine + offset;
|
|
381
|
+
}
|
|
329
382
|
/** Returns true if the given remote URL belongs to this Bitbucket instance. */
|
|
330
383
|
isRemoteForThisInstance(remoteUrl) {
|
|
331
384
|
return this.remoteMatchesInstance(remoteUrl);
|
|
@@ -494,11 +547,17 @@ export class BitbucketClient {
|
|
|
494
547
|
collectAttachmentRefs(pr.description, 'description', attachmentRefs);
|
|
495
548
|
const reviewers = pr.reviewers.map((r) => `${r.user.displayName}${r.approved ? ' ✓' : ''}`).join(', ');
|
|
496
549
|
const url = pr.links?.self?.[0]?.href;
|
|
550
|
+
const fromHash = pr.toRef.latestCommit;
|
|
551
|
+
const toHash = pr.fromRef.latestCommit;
|
|
552
|
+
const commitsLine = fromHash && toHash
|
|
553
|
+
? `Commits: fromHash=${fromHash} toHash=${toHash} (pass to bitbucket_comment to anchor inline comments to this exact state)`
|
|
554
|
+
: '';
|
|
497
555
|
const header = [
|
|
498
556
|
`PR #${pr.id}: ${pr.title}`,
|
|
499
557
|
`State: ${pr.state}`,
|
|
500
558
|
`Author: ${pr.author.user.displayName}`,
|
|
501
559
|
`Branch: ${pr.fromRef.displayId} → ${pr.toRef.displayId}`,
|
|
560
|
+
commitsLine,
|
|
502
561
|
`Reviewers: ${reviewers || 'None'}`,
|
|
503
562
|
url ? `URL: ${url}` : '',
|
|
504
563
|
'',
|
|
@@ -535,13 +594,15 @@ export class BitbucketClient {
|
|
|
535
594
|
if (includeComments) {
|
|
536
595
|
const commentsLimit = args.commentsLimit ?? 50;
|
|
537
596
|
const commentsStart = args.commentsStart ?? 0;
|
|
538
|
-
const commentsState = args.commentsState ?? '
|
|
597
|
+
const commentsState = args.commentsState ?? 'ALL';
|
|
539
598
|
const commentsSeverity = args.commentsSeverity ?? 'ALL';
|
|
540
599
|
if (commentsSeverity === 'BLOCKER' && commentsState === 'PENDING') {
|
|
541
|
-
throw new Error('commentsState=PENDING is not valid when commentsSeverity=BLOCKER. Use OPEN or
|
|
600
|
+
throw new Error('commentsState=PENDING is not valid when commentsSeverity=BLOCKER. Use OPEN, RESOLVED, or ALL.');
|
|
542
601
|
}
|
|
543
602
|
if (commentsSeverity === 'BLOCKER') {
|
|
544
|
-
const qs = new URLSearchParams({ limit: String(commentsLimit), start: String(commentsStart)
|
|
603
|
+
const qs = new URLSearchParams({ limit: String(commentsLimit), start: String(commentsStart) });
|
|
604
|
+
if (commentsState !== 'ALL')
|
|
605
|
+
qs.set('state', commentsState);
|
|
545
606
|
const data = await this.request('GET', `${this.rp(projectKey, repoSlug)}/pull-requests/${prId}/blocker-comments?${qs}`);
|
|
546
607
|
if (!data || data.values.length === 0) {
|
|
547
608
|
sections.push(`Comments:\n(no ${commentsState} BLOCKER comments)`);
|
|
@@ -556,7 +617,7 @@ export class BitbucketClient {
|
|
|
556
617
|
else {
|
|
557
618
|
const activityData = await this.request('GET', `${this.rp(projectKey, repoSlug)}/pull-requests/${prId}/activities?limit=${commentsLimit}&start=${commentsStart}`);
|
|
558
619
|
const comments = uniqueCommentsFromActivities(activityData?.values ?? []).filter((comment) => {
|
|
559
|
-
const matchesState = commentMatchesState(comment, commentsState);
|
|
620
|
+
const matchesState = commentsState === 'ALL' ? true : commentMatchesState(comment, commentsState);
|
|
560
621
|
return matchesState && commentMatchesSeverity(comment, commentsSeverity);
|
|
561
622
|
});
|
|
562
623
|
for (const comment of comments)
|
|
@@ -567,7 +628,7 @@ export class BitbucketClient {
|
|
|
567
628
|
else {
|
|
568
629
|
const blocks = comments.flatMap((comment) => formatCommentThread(comment));
|
|
569
630
|
const paging = activityData ? pageHint(activityData) : '';
|
|
570
|
-
sections.push(`Comments (${comments.length} thread(s))${paging}:\n\n${blocks.join('\n\n')}`);
|
|
631
|
+
sections.push(`Comments (${comments.length} thread(s), newest first)${paging}:\n\n${blocks.join('\n\n')}`);
|
|
571
632
|
}
|
|
572
633
|
}
|
|
573
634
|
}
|
|
@@ -946,7 +1007,9 @@ export class BitbucketClient {
|
|
|
946
1007
|
|| args.lineType !== undefined
|
|
947
1008
|
|| args.fileType !== undefined
|
|
948
1009
|
|| args.multilineStartLine !== undefined
|
|
949
|
-
|| args.multilineStartLineType !== undefined
|
|
1010
|
+
|| args.multilineStartLineType !== undefined
|
|
1011
|
+
|| args.fromHash !== undefined
|
|
1012
|
+
|| args.toHash !== undefined)) {
|
|
950
1013
|
throw new Error('Replies must target an existing comment thread only. Omit filePath/line and other anchor fields when replying.');
|
|
951
1014
|
}
|
|
952
1015
|
if (args.text === undefined && args.suggestion === undefined) {
|
|
@@ -971,11 +1034,14 @@ export class BitbucketClient {
|
|
|
971
1034
|
if (replyToCommentId !== undefined)
|
|
972
1035
|
body.parent = { id: replyToCommentId };
|
|
973
1036
|
let inlineAnchor;
|
|
1037
|
+
let usedFallbackHashes = false;
|
|
1038
|
+
let currentToHash;
|
|
1039
|
+
let currentFromHash;
|
|
1040
|
+
let remapNote;
|
|
974
1041
|
if (args.filePath !== undefined || args.line !== undefined) {
|
|
975
1042
|
if (args.filePath === undefined || args.line === undefined) {
|
|
976
1043
|
throw new Error('filePath and line must be provided together for inline comments.');
|
|
977
1044
|
}
|
|
978
|
-
const pr = await this.request('GET', `${this.rp(projectKey, repoSlug)}/pull-requests/${args.prId}`);
|
|
979
1045
|
inlineAnchor = {
|
|
980
1046
|
diffType: 'EFFECTIVE',
|
|
981
1047
|
fileType: args.fileType ?? 'TO',
|
|
@@ -986,14 +1052,49 @@ export class BitbucketClient {
|
|
|
986
1052
|
if (args.srcPath !== undefined) {
|
|
987
1053
|
inlineAnchor.srcPath = args.srcPath;
|
|
988
1054
|
}
|
|
989
|
-
const
|
|
990
|
-
|
|
1055
|
+
const pr = await this.request('GET', `${this.rp(projectKey, repoSlug)}/pull-requests/${args.prId}`).catch(() => null);
|
|
1056
|
+
currentToHash = pr?.fromRef.latestCommit;
|
|
1057
|
+
currentFromHash = pr?.toRef.latestCommit;
|
|
1058
|
+
let fromHash = args.fromHash ?? currentFromHash;
|
|
1059
|
+
let toHash = args.toHash ?? currentToHash;
|
|
1060
|
+
usedFallbackHashes = args.fromHash === undefined && args.toHash === undefined;
|
|
1061
|
+
const fileType = args.fileType ?? 'TO';
|
|
1062
|
+
const reviewedToHash = args.toHash;
|
|
1063
|
+
if (reviewedToHash
|
|
1064
|
+
&& currentToHash
|
|
1065
|
+
&& reviewedToHash !== currentToHash
|
|
1066
|
+
&& fileType === 'TO') {
|
|
1067
|
+
const remappedLine = await this.remapLineThroughDiff(projectKey, repoSlug, args.filePath, reviewedToHash, currentToHash, args.line);
|
|
1068
|
+
let remappedMultilineStart;
|
|
1069
|
+
if (args.multilineStartLine !== undefined) {
|
|
1070
|
+
remappedMultilineStart = await this.remapLineThroughDiff(projectKey, repoSlug, args.filePath, reviewedToHash, currentToHash, args.multilineStartLine);
|
|
1071
|
+
}
|
|
1072
|
+
const lineOk = remappedLine !== null;
|
|
1073
|
+
const multilineOk = remappedMultilineStart === undefined || remappedMultilineStart !== null;
|
|
1074
|
+
if (lineOk && multilineOk) {
|
|
1075
|
+
if (remappedLine !== args.line) {
|
|
1076
|
+
remapNote = `Reviewed line ${args.line} remapped to ${remappedLine} on current head ${currentToHash.slice(0, 8)}.`;
|
|
1077
|
+
}
|
|
1078
|
+
inlineAnchor.line = remappedLine;
|
|
1079
|
+
if (remappedMultilineStart !== undefined && remappedMultilineStart !== null) {
|
|
1080
|
+
inlineAnchor.multilineStartLine = remappedMultilineStart;
|
|
1081
|
+
}
|
|
1082
|
+
toHash = currentToHash;
|
|
1083
|
+
if (!args.fromHash)
|
|
1084
|
+
fromHash = currentFromHash;
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
remapNote = `Reviewed line ${args.line} was modified or removed in interim commits; anchoring to reviewed commit ${reviewedToHash.slice(0, 8)} (Bitbucket will mark the comment outdated, which is correct — the line you reviewed no longer exists at current head).`;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
991
1090
|
if (fromHash && toHash) {
|
|
992
1091
|
inlineAnchor.fromHash = fromHash;
|
|
993
1092
|
inlineAnchor.toHash = toHash;
|
|
994
1093
|
}
|
|
995
|
-
if (args.multilineStartLine !== undefined) {
|
|
1094
|
+
if (args.multilineStartLine !== undefined && inlineAnchor.multilineStartLine === undefined) {
|
|
996
1095
|
inlineAnchor.multilineStartLine = args.multilineStartLine;
|
|
1096
|
+
}
|
|
1097
|
+
if (inlineAnchor.multilineStartLine !== undefined) {
|
|
997
1098
|
inlineAnchor.multilineStartLineType = args.multilineStartLineType ?? args.lineType ?? 'ADDED';
|
|
998
1099
|
}
|
|
999
1100
|
body.anchor = inlineAnchor;
|
|
@@ -1017,7 +1118,14 @@ export class BitbucketClient {
|
|
|
1017
1118
|
return text(`Reply #${created.id} added to comment #${replyToCommentId} on PR #${args.prId}.`);
|
|
1018
1119
|
}
|
|
1019
1120
|
const location = args.filePath && args.line ? ` on ${args.filePath}:${args.line}` : '';
|
|
1020
|
-
|
|
1121
|
+
const warnings = [];
|
|
1122
|
+
if (inlineAnchor && usedFallbackHashes) {
|
|
1123
|
+
warnings.push('No fromHash/toHash passed — anchored to latest PR head. If you reviewed an older commit, the line may now point at unrelated code. Pass fromHash/toHash from bitbucket_pr_diff or bitbucket_get_pr to bind comments to the exact commit you reviewed.');
|
|
1124
|
+
}
|
|
1125
|
+
if (remapNote)
|
|
1126
|
+
warnings.push(remapNote);
|
|
1127
|
+
const warnSuffix = warnings.length ? `\n\nNote: ${warnings.join(' ')}` : '';
|
|
1128
|
+
return text(`Comment #${created.id} added to PR #${args.prId}${location}.${warnSuffix}`);
|
|
1021
1129
|
}
|
|
1022
1130
|
async updatePrComment(args) {
|
|
1023
1131
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
package/dist/index.js
CHANGED
|
@@ -372,7 +372,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
372
372
|
includeComments: { type: 'boolean', description: 'Include review comments and blockers (default true)', default: true },
|
|
373
373
|
includeDiff: { type: 'boolean', description: 'Include diff text (default false)', default: false },
|
|
374
374
|
includeBuildStatus: { type: 'boolean', description: 'Include CI/build status for the head commit (default true)', default: true },
|
|
375
|
-
commentsState: { type: 'string', enum: ['OPEN', 'RESOLVED', 'PENDING'], description: 'Comment state filter (default OPEN
|
|
375
|
+
commentsState: { type: 'string', enum: ['ALL', 'OPEN', 'RESOLVED', 'PENDING'], description: 'Comment state filter (default ALL — returns every comment with its state badge so nothing is silently hidden). Pass OPEN/RESOLVED only when explicitly narrowing.', default: 'ALL' },
|
|
376
376
|
commentsSeverity: { type: 'string', enum: ['ALL', 'NORMAL', 'BLOCKER'], description: 'Comment severity filter (default ALL)', default: 'ALL' },
|
|
377
377
|
commentsLimit: { type: 'number', description: 'Max comments (default 50)', default: 50 },
|
|
378
378
|
commentsStart: { type: 'number', description: 'Comment pagination offset (default 0)', default: 0 },
|
|
@@ -422,7 +422,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
422
422
|
},
|
|
423
423
|
{
|
|
424
424
|
name: 'bitbucket_comment',
|
|
425
|
-
description: `Add, update, or delete a PR comment. action defaults to "add". For code changes, ALWAYS use inline comments with suggestion when exact replacement code is available. Keep any explanatory text before the suggestion block only (never after), or Bitbucket may hide Apply suggestion. Replies MUST use commentId. Keep comments concise, no emojis. Only call proactively (without being asked) when you are a reviewer on the PR (i.e. "Viewing as" says "you are a reviewer") — never post unsolicited comments on PRs you authored.`,
|
|
425
|
+
description: `Add, update, or delete a PR comment. action defaults to "add". For code changes, ALWAYS use inline comments with suggestion when exact replacement code is available. Keep any explanatory text before the suggestion block only (never after), or Bitbucket may hide Apply suggestion. Replies MUST use commentId. Keep comments concise, no emojis. Only call proactively (without being asked) when you are a reviewer on the PR (i.e. "Viewing as" says "you are a reviewer") — never post unsolicited comments on PRs you authored. For inline comments: ALWAYS pass fromHash + toHash matching the commit you actually reviewed (read from bitbucket_pr_diff or bitbucket_get_pr output). Without them the anchor falls back to current PR head, and if the branch advanced between review and post the line number will point at unrelated code.`,
|
|
426
426
|
inputSchema: {
|
|
427
427
|
type: 'object',
|
|
428
428
|
properties: {
|
|
@@ -441,6 +441,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
441
441
|
fileType: { type: 'string', enum: ['TO', 'FROM'], description: 'Diff side: TO (new, default) or FROM (old)' },
|
|
442
442
|
multilineStartLine: { type: 'number', description: 'First line of multiline anchor (pair with line as last line)' },
|
|
443
443
|
multilineStartLineType: { type: 'string', enum: ['ADDED', 'REMOVED', 'CONTEXT'], description: 'Line type for multilineStartLine' },
|
|
444
|
+
fromHash: { type: 'string', description: 'Base/target commit of the diff you reviewed (from bitbucket_pr_diff or bitbucket_get_pr). Pair with toHash so the anchor binds to the exact commit, not whatever the PR head happens to be at post time.' },
|
|
445
|
+
toHash: { type: 'string', description: 'Source/feature commit of the diff you reviewed (from bitbucket_pr_diff or bitbucket_get_pr). Pair with fromHash. If the PR has advanced since, Bitbucket will mark the comment outdated — that is the correct behaviour.' },
|
|
444
446
|
suggestion: { type: 'string', description: 'Replacement code to suggest. Use whenever proposing a concrete code change. Posted as the final ```suggestion``` block so Apply suggestion appears. Requires filePath + line.' },
|
|
445
447
|
state: { type: 'string', enum: ['OPEN', 'RESOLVED'], description: 'Task state for BLOCKER comments (update only)' },
|
|
446
448
|
threadResolved: { type: 'boolean', description: 'Resolve/reopen normal comment thread (update only)' },
|