@stubbedev/atlassian-mcp 0.3.2 → 0.3.4
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 +6 -1
- package/dist/bitbucket.js +111 -6
- package/dist/index.js +32 -3
- package/dist/jira.js +92 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,11 +27,12 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
27
27
|
|
|
28
28
|
| Tool | Description |
|
|
29
29
|
|---|---|
|
|
30
|
-
| `jira_search` | Discover resources: `issues`, `projects`, `issue_types`, `boards`, `sprints`, `board_overview`, or `users` via `resource` param |
|
|
30
|
+
| `jira_search` | Discover resources: `issues`, `projects`, `issue_types`, `boards`, `sprints`, `board_overview`, `versions`, or `users` via `resource` param |
|
|
31
31
|
| `jira_get` | Full details for one issue: summary, description, status, sprint, transitions, comments, and attachment list |
|
|
32
32
|
| `jira_get_attachment` | Fetch a Jira attachment by ID; images are auto-resized via sharp and returned inline so the model can see them, text/JSON inline, larger/binary files via `saveTo` |
|
|
33
33
|
| `jira_mutate` | Create, update, transition, comment, link, add to sprint, or log work — all in one call |
|
|
34
34
|
| `jira_comment` | Add, update, or delete a comment on an issue (`action`: `add` / `update` / `delete`) |
|
|
35
|
+
| `jira_version` | Manage fix versions/releases (`action`: `create` / `update` / `release` / `archive` / `delete`) |
|
|
35
36
|
|
|
36
37
|
### Bitbucket
|
|
37
38
|
|
|
@@ -62,6 +63,10 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
|
|
|
62
63
|
- "what's in the current sprint?" → `jira_search` with `resource=board_overview`
|
|
63
64
|
- "move FOO-123 to In Progress" → `jira_mutate` with `transitionName="In Progress"`
|
|
64
65
|
- "log 2h on FOO-123" → `jira_mutate` with `worklog`
|
|
66
|
+
- "create version 9.1.0 in PAY" → `jira_version` with `action=create`, `projectKey=PAY`, `name=9.1.0`
|
|
67
|
+
- "list releases for PAY" → `jira_search` with `resource=versions`, `project=PAY`
|
|
68
|
+
- "release version 12345" → `jira_version` with `action=release`, `id=12345`
|
|
69
|
+
- "set fix version 9.1.0 on FOO-123" → `jira_mutate` with `update.fixVersion=9.1.0`
|
|
65
70
|
|
|
66
71
|
---
|
|
67
72
|
|
package/dist/bitbucket.js
CHANGED
|
@@ -133,11 +133,20 @@ function pageHint(data) {
|
|
|
133
133
|
}
|
|
134
134
|
function formatDiff(data, maxChars = 8000) {
|
|
135
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
|
+
}
|
|
136
140
|
for (const diff of data.diffs) {
|
|
137
141
|
const from = diff.source?.toString ?? '/dev/null';
|
|
138
142
|
const to = diff.destination?.toString ?? '/dev/null';
|
|
139
143
|
parts.push(`--- a/${from}\n+++ b/${to}`);
|
|
140
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} @@`);
|
|
141
150
|
for (const segment of hunk.segments ?? []) {
|
|
142
151
|
const prefix = segment.type === 'ADDED' ? '+' : segment.type === 'REMOVED' ? '-' : ' ';
|
|
143
152
|
for (const line of segment.lines ?? []) {
|
|
@@ -327,6 +336,49 @@ export class BitbucketClient {
|
|
|
327
336
|
}
|
|
328
337
|
return res.status === 204 ? null : res.json();
|
|
329
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
|
+
}
|
|
330
382
|
/** Returns true if the given remote URL belongs to this Bitbucket instance. */
|
|
331
383
|
isRemoteForThisInstance(remoteUrl) {
|
|
332
384
|
return this.remoteMatchesInstance(remoteUrl);
|
|
@@ -495,11 +547,17 @@ export class BitbucketClient {
|
|
|
495
547
|
collectAttachmentRefs(pr.description, 'description', attachmentRefs);
|
|
496
548
|
const reviewers = pr.reviewers.map((r) => `${r.user.displayName}${r.approved ? ' ✓' : ''}`).join(', ');
|
|
497
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
|
+
: '';
|
|
498
555
|
const header = [
|
|
499
556
|
`PR #${pr.id}: ${pr.title}`,
|
|
500
557
|
`State: ${pr.state}`,
|
|
501
558
|
`Author: ${pr.author.user.displayName}`,
|
|
502
559
|
`Branch: ${pr.fromRef.displayId} → ${pr.toRef.displayId}`,
|
|
560
|
+
commitsLine,
|
|
503
561
|
`Reviewers: ${reviewers || 'None'}`,
|
|
504
562
|
url ? `URL: ${url}` : '',
|
|
505
563
|
'',
|
|
@@ -949,7 +1007,9 @@ export class BitbucketClient {
|
|
|
949
1007
|
|| args.lineType !== undefined
|
|
950
1008
|
|| args.fileType !== undefined
|
|
951
1009
|
|| args.multilineStartLine !== undefined
|
|
952
|
-
|| args.multilineStartLineType !== undefined
|
|
1010
|
+
|| args.multilineStartLineType !== undefined
|
|
1011
|
+
|| args.fromHash !== undefined
|
|
1012
|
+
|| args.toHash !== undefined)) {
|
|
953
1013
|
throw new Error('Replies must target an existing comment thread only. Omit filePath/line and other anchor fields when replying.');
|
|
954
1014
|
}
|
|
955
1015
|
if (args.text === undefined && args.suggestion === undefined) {
|
|
@@ -974,11 +1034,14 @@ export class BitbucketClient {
|
|
|
974
1034
|
if (replyToCommentId !== undefined)
|
|
975
1035
|
body.parent = { id: replyToCommentId };
|
|
976
1036
|
let inlineAnchor;
|
|
1037
|
+
let usedFallbackHashes = false;
|
|
1038
|
+
let currentToHash;
|
|
1039
|
+
let currentFromHash;
|
|
1040
|
+
let remapNote;
|
|
977
1041
|
if (args.filePath !== undefined || args.line !== undefined) {
|
|
978
1042
|
if (args.filePath === undefined || args.line === undefined) {
|
|
979
1043
|
throw new Error('filePath and line must be provided together for inline comments.');
|
|
980
1044
|
}
|
|
981
|
-
const pr = await this.request('GET', `${this.rp(projectKey, repoSlug)}/pull-requests/${args.prId}`);
|
|
982
1045
|
inlineAnchor = {
|
|
983
1046
|
diffType: 'EFFECTIVE',
|
|
984
1047
|
fileType: args.fileType ?? 'TO',
|
|
@@ -989,14 +1052,49 @@ export class BitbucketClient {
|
|
|
989
1052
|
if (args.srcPath !== undefined) {
|
|
990
1053
|
inlineAnchor.srcPath = args.srcPath;
|
|
991
1054
|
}
|
|
992
|
-
const
|
|
993
|
-
|
|
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
|
+
}
|
|
994
1090
|
if (fromHash && toHash) {
|
|
995
1091
|
inlineAnchor.fromHash = fromHash;
|
|
996
1092
|
inlineAnchor.toHash = toHash;
|
|
997
1093
|
}
|
|
998
|
-
if (args.multilineStartLine !== undefined) {
|
|
1094
|
+
if (args.multilineStartLine !== undefined && inlineAnchor.multilineStartLine === undefined) {
|
|
999
1095
|
inlineAnchor.multilineStartLine = args.multilineStartLine;
|
|
1096
|
+
}
|
|
1097
|
+
if (inlineAnchor.multilineStartLine !== undefined) {
|
|
1000
1098
|
inlineAnchor.multilineStartLineType = args.multilineStartLineType ?? args.lineType ?? 'ADDED';
|
|
1001
1099
|
}
|
|
1002
1100
|
body.anchor = inlineAnchor;
|
|
@@ -1020,7 +1118,14 @@ export class BitbucketClient {
|
|
|
1020
1118
|
return text(`Reply #${created.id} added to comment #${replyToCommentId} on PR #${args.prId}.`);
|
|
1021
1119
|
}
|
|
1022
1120
|
const location = args.filePath && args.line ? ` on ${args.filePath}:${args.line}` : '';
|
|
1023
|
-
|
|
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}`);
|
|
1024
1129
|
}
|
|
1025
1130
|
async updatePrComment(args) {
|
|
1026
1131
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
package/dist/index.js
CHANGED
|
@@ -206,11 +206,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
206
206
|
},
|
|
207
207
|
{
|
|
208
208
|
name: 'jira_search',
|
|
209
|
-
description: 'Discover Jira resources. Use when asked "find tickets for...", "what\'s in the backlog", "show me my issues", "list projects", or "which board is for project X". Set resource:\n• "issues" (default) — search by text, JQL, project, status, assignee, issue type, or mine=true for your queue\n• "projects" — list all projects and their keys\n• "issue_types" — valid types and statuses for a project\n• "boards" — list boards (pass project to filter by project key); use this to find the boardId before fetching sprints or board_overview\n• "sprints" — sprints for a board (pass boardId); if you don\'t know the boardId, first use resource=boards\n• "board_overview" — active/future sprints with their issues for a board (pass boardId); use when asked "what\'s in the sprint", "show me the board", or "what\'s everyone working on"\n• "users" — find users by name/email (pass query)',
|
|
209
|
+
description: 'Discover Jira resources. Use when asked "find tickets for...", "what\'s in the backlog", "show me my issues", "list projects", or "which board is for project X". Set resource:\n• "issues" (default) — search by text, JQL, project, status, assignee, issue type, or mine=true for your queue\n• "projects" — list all projects and their keys\n• "issue_types" — valid types and statuses for a project\n• "boards" — list boards (pass project to filter by project key); use this to find the boardId before fetching sprints or board_overview\n• "sprints" — sprints for a board (pass boardId); if you don\'t know the boardId, first use resource=boards\n• "board_overview" — active/future sprints with their issues for a board (pass boardId); use when asked "what\'s in the sprint", "show me the board", or "what\'s everyone working on"\n• "versions" — list fix versions/releases for a project (pass project); use this to find the exact version name or id before setting fixVersion or releasing a version\n• "users" — find users by name/email (pass query)',
|
|
210
210
|
inputSchema: {
|
|
211
211
|
type: 'object',
|
|
212
212
|
properties: {
|
|
213
|
-
resource: { type: 'string', enum: ['issues', 'projects', 'issue_types', 'boards', 'sprints', 'board_overview', 'users'], description: 'What to search (default: issues)' },
|
|
213
|
+
resource: { type: 'string', enum: ['issues', 'projects', 'issue_types', 'boards', 'sprints', 'board_overview', 'versions', 'users'], description: 'What to search (default: issues)' },
|
|
214
214
|
mine: { type: 'boolean', description: 'Return issues assigned to you (resource=issues only)' },
|
|
215
215
|
query: { type: 'string', description: 'Text search or user name query' },
|
|
216
216
|
jql: { type: 'string', description: 'Raw JQL (resource=issues only, overrides other filters)' },
|
|
@@ -331,6 +331,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
331
331
|
},
|
|
332
332
|
required: ['issueKey'],
|
|
333
333
|
},
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'jira_version',
|
|
337
|
+
description: 'Manage Jira fix versions (releases). Use when asked to "create version 9.1.0", "release version X", "archive version X", "rename a version", or "delete a version". action defaults to "create". For create pass projectKey + name. For update/release/archive/delete pass id (look it up via jira_search resource=versions). "release" sets released=true and defaults releaseDate to today. Once a version exists you can set it on tickets via jira_mutate update.fixVersion.',
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties: {
|
|
341
|
+
action: { type: 'string', enum: ['create', 'update', 'release', 'archive', 'delete'], description: 'Operation (default: create)' },
|
|
342
|
+
projectKey: { type: 'string', description: 'Jira project code (required for create when not auto-resolvable)' },
|
|
343
|
+
project: { type: 'string', description: 'Alias for projectKey' },
|
|
344
|
+
id: { type: 'string', description: 'Version id (required for update/release/archive/delete; look up via jira_search resource=versions)' },
|
|
345
|
+
name: { type: 'string', description: 'Version name, e.g. "9.1.0" (required for create; optional rename for update)' },
|
|
346
|
+
description: { type: 'string', description: 'Version description (optional)' },
|
|
347
|
+
startDate: { type: 'string', description: 'Start date in YYYY-MM-DD (optional)' },
|
|
348
|
+
releaseDate: { type: 'string', description: 'Release date in YYYY-MM-DD (optional; defaults to today on action=release)' },
|
|
349
|
+
released: { type: 'boolean', description: 'Released flag (optional; action=release forces true)' },
|
|
350
|
+
archived: { type: 'boolean', description: 'Archived flag (optional; action=archive forces true)' },
|
|
351
|
+
},
|
|
352
|
+
},
|
|
334
353
|
}
|
|
335
354
|
] : []),
|
|
336
355
|
...(bitbucket ? [{
|
|
@@ -422,7 +441,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
422
441
|
},
|
|
423
442
|
{
|
|
424
443
|
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.`,
|
|
444
|
+
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
445
|
inputSchema: {
|
|
427
446
|
type: 'object',
|
|
428
447
|
properties: {
|
|
@@ -441,6 +460,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
441
460
|
fileType: { type: 'string', enum: ['TO', 'FROM'], description: 'Diff side: TO (new, default) or FROM (old)' },
|
|
442
461
|
multilineStartLine: { type: 'number', description: 'First line of multiline anchor (pair with line as last line)' },
|
|
443
462
|
multilineStartLineType: { type: 'string', enum: ['ADDED', 'REMOVED', 'CONTEXT'], description: 'Line type for multilineStartLine' },
|
|
463
|
+
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.' },
|
|
464
|
+
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
465
|
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
466
|
state: { type: 'string', enum: ['OPEN', 'RESOLVED'], description: 'Task state for BLOCKER comments (update only)' },
|
|
446
467
|
threadResolved: { type: 'boolean', description: 'Resolve/reopen normal comment thread (update only)' },
|
|
@@ -756,6 +777,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
756
777
|
return await jira.getSprints({ boardId: a.boardId, state: a.sprintState, maxResults: a.maxResults, startAt: a.startAt });
|
|
757
778
|
if (resource === 'board_overview')
|
|
758
779
|
return await jira.boardOverview({ boardId: a.boardId, sprintState: a.sprintState, sprintMaxResults: a.maxResults, sprintStartAt: a.startAt, includeIssues: a.includeIssues, assignee: a.assignee, status: a.status });
|
|
780
|
+
if (resource === 'versions')
|
|
781
|
+
return await jira.listVersions({ projectKey: a.projectKey ?? a.project, maxResults: a.maxResults });
|
|
759
782
|
if (resource === 'users')
|
|
760
783
|
return await jira.searchUsers({ query: a.query ?? '', maxResults: a.maxResults });
|
|
761
784
|
// issues (default)
|
|
@@ -788,6 +811,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
788
811
|
return await jira.deleteComment({ issueKey: a.issueKey, commentId: a.commentId });
|
|
789
812
|
return await jira.addComment({ issueKey: a.issueKey, body: a.body });
|
|
790
813
|
}
|
|
814
|
+
case 'jira_version': {
|
|
815
|
+
if (!jira)
|
|
816
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
817
|
+
const a = normalizeJiraProjectArgs(args);
|
|
818
|
+
return await jira.mutateVersion(a);
|
|
819
|
+
}
|
|
791
820
|
// Bitbucket
|
|
792
821
|
case 'bitbucket_search': {
|
|
793
822
|
if (!bitbucket)
|
package/dist/jira.js
CHANGED
|
@@ -761,4 +761,96 @@ export class JiraClient {
|
|
|
761
761
|
await this.transitionIssueInternal(args.issueKey, transitionId);
|
|
762
762
|
return text(`Transitioned ${args.issueKey} using transition ${transitionId}.\n${this.issueUrl(args.issueKey)}`);
|
|
763
763
|
}
|
|
764
|
+
async listVersions(args) {
|
|
765
|
+
const projectKey = await this.resolveProjectKey(args.projectKey);
|
|
766
|
+
const data = await this.request('GET', `/project/${encodeURIComponent(projectKey)}/versions`);
|
|
767
|
+
if (!data || data.length === 0)
|
|
768
|
+
return text(`No versions in ${projectKey}.`);
|
|
769
|
+
const sorted = [...data].sort((a, b) => {
|
|
770
|
+
if (a.released !== b.released)
|
|
771
|
+
return a.released ? 1 : -1;
|
|
772
|
+
if (a.archived !== b.archived)
|
|
773
|
+
return a.archived ? 1 : -1;
|
|
774
|
+
const ad = a.releaseDate ?? '';
|
|
775
|
+
const bd = b.releaseDate ?? '';
|
|
776
|
+
return bd.localeCompare(ad);
|
|
777
|
+
});
|
|
778
|
+
const limit = args.maxResults ?? sorted.length;
|
|
779
|
+
const shown = sorted.slice(0, limit);
|
|
780
|
+
const lines = shown.map((v, i) => {
|
|
781
|
+
const tags = [];
|
|
782
|
+
if (v.released)
|
|
783
|
+
tags.push('released');
|
|
784
|
+
if (v.archived)
|
|
785
|
+
tags.push('archived');
|
|
786
|
+
const tagStr = tags.length ? ` [${tags.join(', ')}]` : '';
|
|
787
|
+
const dateParts = [v.startDate ? `start ${v.startDate}` : '', v.releaseDate ? `release ${v.releaseDate}` : ''].filter(Boolean);
|
|
788
|
+
const dateStr = dateParts.length ? ` (${dateParts.join(', ')})` : '';
|
|
789
|
+
return `${i + 1}. [${v.id}] ${v.name}${tagStr}${dateStr}`;
|
|
790
|
+
});
|
|
791
|
+
const more = data.length > shown.length ? `\n...and ${data.length - shown.length} more (raise maxResults).` : '';
|
|
792
|
+
return text(`${data.length} version(s) in ${projectKey}:\n${lines.join('\n')}${more}`);
|
|
793
|
+
}
|
|
794
|
+
async mutateVersion(args) {
|
|
795
|
+
const action = args.action ?? 'create';
|
|
796
|
+
if (action === 'create') {
|
|
797
|
+
const projectKey = await this.resolveProjectKey(args.projectKey);
|
|
798
|
+
const name = args.name?.trim();
|
|
799
|
+
if (!name)
|
|
800
|
+
throw new Error('name is required to create a version.');
|
|
801
|
+
const body = { project: projectKey, name };
|
|
802
|
+
if (args.description !== undefined)
|
|
803
|
+
body.description = args.description;
|
|
804
|
+
if (args.releaseDate)
|
|
805
|
+
body.releaseDate = args.releaseDate;
|
|
806
|
+
if (args.startDate)
|
|
807
|
+
body.startDate = args.startDate;
|
|
808
|
+
if (args.released !== undefined)
|
|
809
|
+
body.released = args.released;
|
|
810
|
+
if (args.archived !== undefined)
|
|
811
|
+
body.archived = args.archived;
|
|
812
|
+
const created = await this.request('POST', '/version', body);
|
|
813
|
+
if (!created)
|
|
814
|
+
throw new Error('Jira returned no body when creating version.');
|
|
815
|
+
return text(`Created version [${created.id}] ${created.name} in ${projectKey}.`);
|
|
816
|
+
}
|
|
817
|
+
const id = args.id?.trim();
|
|
818
|
+
if (!id)
|
|
819
|
+
throw new Error(`version id is required for action=${action}.`);
|
|
820
|
+
if (action === 'delete') {
|
|
821
|
+
await this.request('DELETE', `/version/${encodeURIComponent(id)}`);
|
|
822
|
+
return text(`Deleted version ${id}.`);
|
|
823
|
+
}
|
|
824
|
+
const body = {};
|
|
825
|
+
if (args.name !== undefined)
|
|
826
|
+
body.name = args.name;
|
|
827
|
+
if (args.description !== undefined)
|
|
828
|
+
body.description = args.description;
|
|
829
|
+
if (args.startDate !== undefined)
|
|
830
|
+
body.startDate = args.startDate;
|
|
831
|
+
if (args.releaseDate !== undefined)
|
|
832
|
+
body.releaseDate = args.releaseDate;
|
|
833
|
+
if (args.released !== undefined)
|
|
834
|
+
body.released = args.released;
|
|
835
|
+
if (args.archived !== undefined)
|
|
836
|
+
body.archived = args.archived;
|
|
837
|
+
if (action === 'release') {
|
|
838
|
+
body.released = true;
|
|
839
|
+
if (body.releaseDate === undefined)
|
|
840
|
+
body.releaseDate = new Date().toISOString().slice(0, 10);
|
|
841
|
+
}
|
|
842
|
+
else if (action === 'archive') {
|
|
843
|
+
body.archived = true;
|
|
844
|
+
}
|
|
845
|
+
if (Object.keys(body).length === 0) {
|
|
846
|
+
throw new Error('Nothing to update.');
|
|
847
|
+
}
|
|
848
|
+
const updated = await this.request('PUT', `/version/${encodeURIComponent(id)}`, body);
|
|
849
|
+
const label = updated ? `[${updated.id}] ${updated.name}` : id;
|
|
850
|
+
if (action === 'release')
|
|
851
|
+
return text(`Released version ${label} on ${body.releaseDate}.`);
|
|
852
|
+
if (action === 'archive')
|
|
853
|
+
return text(`Archived version ${label}.`);
|
|
854
|
+
return text(`Updated version ${label}.`);
|
|
855
|
+
}
|
|
764
856
|
}
|