@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 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 fromHash = pr?.toRef.latestCommit;
993
- const toHash = pr?.fromRef.latestCommit;
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
- return text(`Comment #${created.id} added to PR #${args.prId}${location}.`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
5
  "license": "MIT",
6
6
  "type": "module",