@stubbedev/atlassian-mcp 0.4.1 → 0.4.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 CHANGED
@@ -63,18 +63,32 @@ function branchDisplayId(branch) {
63
63
  function formatDate(ms) {
64
64
  return new Date(ms).toISOString().slice(0, 10);
65
65
  }
66
+ // Cap long free-text (e.g. PR descriptions) so one verbose PR does not flood
67
+ // the model's context. Returns the text untouched when within cap.
68
+ function capText(value, max) {
69
+ if (max <= 0 || value.length <= max)
70
+ return value;
71
+ const more = value.length - max;
72
+ return `${value.slice(0, max)}\n... (truncated, ${more} more chars — pass fullDescription=true for the rest)`;
73
+ }
66
74
  function formatCommentThread(comment, indent = '', depth = 0) {
67
75
  if (depth > 20)
68
76
  return [`${indent}... (deeply nested replies omitted)`];
69
77
  const author = comment.author?.displayName ?? comment.author?.name ?? 'Unknown';
70
78
  const date = comment.createdDate ? ` (${formatDate(comment.createdDate)})` : '';
71
- const state = comment.state ?? 'OPEN';
72
- const severity = comment.severity ?? 'NORMAL';
73
- const threadStatus = comment.threadResolved !== undefined
74
- ? ` thread=${comment.threadResolved ? 'RESOLVED' : 'OPEN'}`
75
- : '';
79
+ // Show only non-default flags: OPEN state, NORMAL severity and unresolved
80
+ // threads are the implied baseline, so badge only what deviates. Keeps the
81
+ // RESOLVED/BLOCKER signal while dropping repeated [OPEN/NORMAL thread=OPEN].
82
+ const flags = [];
83
+ if ((comment.state ?? 'OPEN') !== 'OPEN')
84
+ flags.push(comment.state);
85
+ if ((comment.severity ?? 'NORMAL') !== 'NORMAL')
86
+ flags.push(comment.severity);
87
+ if (comment.threadResolved === true)
88
+ flags.push('thread=RESOLVED');
89
+ const flagStr = flags.length > 0 ? ` [${flags.join('/')}]` : '';
76
90
  const lines = [
77
- `${indent}#${comment.id} [${state}/${severity}${threadStatus}] ${author}${date} (v${comment.version})`,
91
+ `${indent}#${comment.id}${flagStr} ${author}${date} (v${comment.version})`,
78
92
  `${indent}${comment.text}`,
79
93
  ];
80
94
  if (comment.comments && comment.comments.length > 0) {
@@ -529,6 +543,7 @@ export class BitbucketClient {
529
543
  const includeComments = args.includeComments ?? true;
530
544
  const includeDiff = args.includeDiff ?? false;
531
545
  const includeBuildStatus = args.includeBuildStatus ?? true;
546
+ const descriptionCap = args.fullDescription ? 0 : args.descriptionMaxChars ?? 2000;
532
547
  let prId = args.prId;
533
548
  if (prId === undefined) {
534
549
  const branch = args.fromBranch ?? safeExec('git rev-parse --abbrev-ref HEAD');
@@ -567,7 +582,7 @@ export class BitbucketClient {
567
582
  url ? `URL: ${url}` : '',
568
583
  '',
569
584
  'Description:',
570
- pr.description ?? '(no description)',
585
+ pr.description ? capText(pr.description, descriptionCap) : '(no description)',
571
586
  ].filter((line) => line !== '');
572
587
  sections.push(header.join('\n'));
573
588
  if (includeBuildStatus && pr.fromRef.latestCommit) {
package/dist/context.js CHANGED
@@ -102,6 +102,7 @@ export async function getDevContext(args, jira, bitbucket) {
102
102
  commentsMaxResults: 5,
103
103
  includeTransitions: true,
104
104
  includeSprint: true,
105
+ descriptionMaxChars: 800,
105
106
  });
106
107
  return `── Jira ${key} ──\n${result.content[0].text}`;
107
108
  }
package/dist/index.js CHANGED
@@ -204,7 +204,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
204
204
  // ── Combined context (jira + bitbucket, or either alone) ─────────────
205
205
  ...(jira || bitbucket ? [{
206
206
  name: 'get_dev_context',
207
- description: 'Master entry point. Use when asked "what am I working on?", "what\'s the status?", "show me the context", or before any review or coding task. Returns: git branch + upstream state, Jira ticket overview (status, transitions, sprint, comments), open PR with reviewer approvals, and actionable next-step hints (create PR, merge, address blockers).',
207
+ description: 'Master entry point for "what am I working on / what\'s the status", and before any review or coding task. Returns: git branch + upstream state, Jira ticket overview (status, transitions, sprint, comments), open PR with reviewer approvals, and actionable next-step hints (create PR, merge, address blockers).',
208
208
  inputSchema: {
209
209
  type: 'object',
210
210
  properties: {
@@ -216,7 +216,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
216
216
  ...(jira ? [
217
217
  {
218
218
  name: 'start_work',
219
- description: 'Start working on a Jira ticket end-to-end: resolves the ticket (by key or free-text search with a picker when multiple match), creates a local branch with an auto-generated name, fetches the project README from Bitbucket so you have commit/PR conventions in context, and prints a ready-to-use next-steps summary. Use when told "make a branch for FOO-123", "start working on this ticket", "I want to work on the login bug", or "begin work on the payment gateway story". If issueKey is omitted, provide query for free-text search.',
219
+ description: 'Start working on a Jira ticket end-to-end: resolves the ticket (by key or free-text search with a picker when multiple match), creates a local branch with an auto-generated name, fetches the project README from Bitbucket so you have commit/PR conventions in context, and prints a next-steps summary. If issueKey is omitted, provide query for free-text search.',
220
220
  inputSchema: {
221
221
  type: 'object',
222
222
  properties: {
@@ -232,7 +232,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
232
232
  },
233
233
  {
234
234
  name: 'jira_search',
235
- 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; optionally pass query to filter by name substring). If the version you need does not exist, create it yourself with `jira_version action=create` — do NOT ask the user to make it in the Jira UI.\n• "users" — find users by name/email (pass query)',
235
+ description: 'Discover Jira resources (tickets, projects, boards, sprints, versions, users). 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; optionally pass query to filter by name substring). If the version you need does not exist, create it yourself with `jira_version action=create` — do NOT ask the user to make it in the Jira UI.\n• "users" — find users by name/email (pass query)',
236
236
  inputSchema: {
237
237
  type: 'object',
238
238
  properties: {
@@ -254,7 +254,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
254
254
  },
255
255
  {
256
256
  name: 'jira_get',
257
- description: 'Full details for one Jira issue: summary, description, status, assignee, sprint, available transitions, recent comments, and a list of attachments (filename, size, mime type, attachment ID). Use when asked to "show me FOO-123", "what does this ticket say", "get the details for this issue", or after discovering a key from get_dev_context or jira_search. To view an attachment\'s contents (e.g. an image), call jira_get_attachment with the attachment ID surfaced here.',
257
+ description: 'Full details for one Jira issue: summary, description, status, assignee, sprint, available transitions, recent comments, and a list of attachments (filename, size, mime type, attachment ID). To view an attachment\'s contents (e.g. an image), call jira_get_attachment with the attachment ID surfaced here.',
258
258
  inputSchema: {
259
259
  type: 'object',
260
260
  properties: {
@@ -264,13 +264,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
264
264
  commentsStartAt: { type: 'number', description: 'Comment pagination offset (default 0)', default: 0 },
265
265
  includeTransitions: { type: 'boolean', description: 'Include available transitions (default true)', default: true },
266
266
  includeSprint: { type: 'boolean', description: 'Include sprint data (default true)', default: true },
267
+ fullDescription: { type: 'boolean', description: 'Return the full description even when long (default false — descriptions over ~2000 chars are truncated to save context)', default: false },
267
268
  },
268
269
  required: ['issueKey'],
269
270
  },
270
271
  },
271
272
  {
272
273
  name: 'jira_mutate',
273
- description: `Use when asked to "create a ticket", "log a bug", "move FOO-123 to In Progress", "close this issue", "assign to X", "add a comment on FOO-123", "FOO-123 blocks BAR-456", "log 2h on this ticket", or "add a sub-task". Bundles create/update/transition/comment/link/worklog in one call. ${JIRA_WIKI_MARKUP_HINT}`,
274
+ description: `Create/update a ticket, transition status, assign, comment, link issues, or log work bundles create/update/transition/comment/link/worklog in one call. ${JIRA_WIKI_MARKUP_HINT}`,
274
275
  inputSchema: {
275
276
  type: 'object',
276
277
  properties: {
@@ -334,7 +335,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
334
335
  },
335
336
  {
336
337
  name: 'jira_get_attachment',
337
- description: 'Fetch a Jira attachment by ID and return its contents inline. Images are auto-resized (long edge ≤1568 px by default) and re-encoded so they fit in context, then returned as image content blocks; text/JSON/XML come back as text. Videos AND animated images (GIF/APNG/animated WebP) are decoded with ffmpeg: by default 6 frames are sampled uniformly across the whole clip (768 px / q65, mpdecimate to drop near-duplicates) — re-call with start/end/frames or mode=scenes to refine. Audio is returned as an MCP audio block. PDFs return extracted text. Anything still too large or non-renderable is automatically saved to a temp file and the path is returned. Use jira_get first to discover attachment IDs.',
338
+ description: 'Fetch a Jira attachment by ID and return its contents inline. Images are auto-resized + re-encoded; text/JSON/XML return as text; videos and animated images (GIF/APNG/animated WebP) are decoded with ffmpeg into sampled frames (re-call with start/end/frames or mode=scenes to refine); audio returns as an audio block; PDFs return extracted text. Oversized/non-renderable files are saved to a temp file and the path returned. Use jira_get first to discover attachment IDs.',
338
339
  inputSchema: {
339
340
  type: 'object',
340
341
  properties: {
@@ -353,7 +354,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
353
354
  },
354
355
  {
355
356
  name: 'jira_comment',
356
- description: `Add, update, or delete a comment on a Jira issue. Use when asked to "edit my comment on FOO-123", "delete comment 12345", or "update that comment". action defaults to "add". Can only edit/delete your own comments. ${JIRA_WIKI_MARKUP_HINT}`,
357
+ description: `Add, update, or delete a comment on a Jira issue. action defaults to "add". Can only edit/delete your own comments. ${JIRA_WIKI_MARKUP_HINT}`,
357
358
  inputSchema: {
358
359
  type: 'object',
359
360
  properties: {
@@ -367,7 +368,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
367
368
  },
368
369
  {
369
370
  name: 'jira_version',
370
- 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.',
371
+ description: 'Manage Jira fix versions (releases): create, update, release, archive, delete. 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.',
371
372
  inputSchema: {
372
373
  type: 'object',
373
374
  properties: {
@@ -387,7 +388,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
387
388
  ] : []),
388
389
  ...(bitbucket ? [{
389
390
  name: 'bitbucket_search',
390
- description: 'Discover Bitbucket resources. Use when asked "what PRs are open?", "show me the repos", "find the PR for this branch", or "list branches". Set resource:\n• "pull_requests" (default) — list PRs by state/branch/text; mine=true for your inbox\n• "repos" — list repositories in a project\n• "branches" — list or filter branches in a repo\n• "users" — find users by name/email (pass query); add projectKey+repoSlug to restrict to users with repo access. ALWAYS use this to look up valid usernames before adding reviewers to a PR.',
391
+ description: 'Discover Bitbucket resources (PRs, repos, branches, users). Set resource:\n• "pull_requests" (default) — list PRs by state/branch/text; mine=true for your inbox\n• "repos" — list repositories in a project\n• "branches" — list or filter branches in a repo\n• "users" — find users by name/email (pass query); add projectKey+repoSlug to restrict to users with repo access. ALWAYS use this to look up valid usernames before adding reviewers to a PR.',
391
392
  inputSchema: {
392
393
  type: 'object',
393
394
  properties: {
@@ -410,7 +411,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
410
411
  },
411
412
  {
412
413
  name: 'bitbucket_get_pr',
413
- description: 'Full details for one PR: metadata, commits, open comments, blockers, optional diff, and any attachments referenced from the description or comments (with attachment ID + filename). Use when asked to "review this PR", "show me the review comments", "what\'s blocking the merge", or after get_dev_context surfaces a prId. IMPORTANT: The PR branch is often not the locally checked-out branch. Do NOT read files with local tools (Read, git_get_diff, etc.) for PR context — use bitbucket_get_file with the PR\'s source branch instead. To view an attachment\'s contents, call bitbucket_get_attachment with the surfaced attachment ID. The response includes a "Viewing as" line — if it says "you are the author", do NOT add review comments or a summary unless explicitly asked; just answer questions about the PR. If it says "you are a reviewer", default to posting inline comments for suggested changes and a final summary comment.',
414
+ description: 'Full details for one PR: metadata, commits, open comments, blockers, optional diff, and any attachments referenced from the description or comments (with attachment ID + filename). IMPORTANT: The PR branch is often not the locally checked-out branch. Do NOT read files with local tools (Read, git_get_diff, etc.) for PR context — use bitbucket_get_file with the PR\'s source branch instead. To view an attachment\'s contents, call bitbucket_get_attachment with the surfaced attachment ID. The response includes a "Viewing as" line — if it says "you are the author", do NOT add review comments or a summary unless explicitly asked; just answer questions about the PR. If it says "you are a reviewer", default to posting inline comments for suggested changes and a final summary comment.',
414
415
  inputSchema: {
415
416
  type: 'object',
416
417
  properties: {
@@ -430,12 +431,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
430
431
  commentsStart: { type: 'number', description: 'Comment pagination offset (default 0)', default: 0 },
431
432
  commitsLimit: { type: 'number', description: 'Max commits (default 25)', default: 25 },
432
433
  diffMaxChars: { type: 'number', description: 'Max diff chars when includeDiff=true (default 8000)', default: 8000 },
434
+ fullDescription: { type: 'boolean', description: 'Return the full PR description even when long (default false — descriptions over ~2000 chars are truncated to save context)', default: false },
433
435
  },
434
436
  },
435
437
  },
436
438
  {
437
439
  name: 'bitbucket_mutate',
438
- description: 'Use when asked to "open a PR for this branch", "create a pull request", "approve this PR", "request changes / mark needs work", "merge it", "ship it", or "decline this PR". Auto-targets the open PR for the current branch when prId is omitted. Handles create, update, approve/unapprove, needs_work, decline, and merge in one call. needs_work sets your reviewer status to "Needs work" (Bitbucket Server\'s changes-requested signal); revert with action=unapprove.',
440
+ description: 'Create, update, approve/unapprove, mark needs_work, decline, or merge a PR in one call. Auto-targets the open PR for the current branch when prId is omitted. needs_work sets your reviewer status to "Needs work" (Bitbucket Server\'s changes-requested signal); revert with action=unapprove.',
439
441
  inputSchema: {
440
442
  type: 'object',
441
443
  properties: {
@@ -521,7 +523,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
521
523
  },
522
524
  {
523
525
  name: 'bitbucket_get_attachment',
524
- description: 'Fetch a Bitbucket repo attachment by ID and return its contents inline. Bitbucket Server attachments are repo-scoped and referenced from PR descriptions/comments via attachment:<id> markdown. Use bitbucket_get_pr first to surface attachment IDs. Images are auto-resized (long edge ≤1568 px by default) and re-encoded, then returned as image content blocks; text/JSON/XML as text. Videos AND animated images (GIF/APNG/animated WebP) are decoded with ffmpeg: by default 6 frames are sampled uniformly across the whole clip (768 px / q65, mpdecimate to drop near-duplicates) — re-call with start/end/frames or mode=scenes to refine. Audio is returned as an MCP audio block. PDFs return extracted text. Oversized/non-renderable attachments are auto-saved to a temp file and the path is returned.',
526
+ description: 'Fetch a Bitbucket repo attachment by ID and return its contents inline. Attachments are repo-scoped, referenced from PR descriptions/comments via attachment:<id> markdown; use bitbucket_get_pr first to surface IDs. Images are auto-resized + re-encoded; text/JSON/XML return as text; videos and animated images (GIF/APNG/animated WebP) are decoded with ffmpeg into sampled frames (re-call with start/end/frames or mode=scenes to refine); audio returns as an audio block; PDFs return extracted text. Oversized/non-renderable files are saved to a temp file and the path returned.',
525
527
  inputSchema: {
526
528
  type: 'object',
527
529
  properties: {
@@ -544,7 +546,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
544
546
  },
545
547
  {
546
548
  name: 'bitbucket_pr_tasks',
547
- description: 'Manage PR tasks (checklist items). Use when asked to "list the tasks on this PR", "create a task for FOO-123", "mark task #5 as done", or "add a checklist item". Tasks are distinct from comments — they appear as a checklist in the PR sidebar.',
549
+ description: 'Manage PR tasks (checklist items): list, create, resolve, reopen, delete. Tasks are distinct from comments — they appear as a checklist in the PR sidebar.',
548
550
  inputSchema: {
549
551
  type: 'object',
550
552
  properties: {
@@ -565,7 +567,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
565
567
  // ── Combined workflow ─────────────────────────────────────────────────
566
568
  ...(jira && bitbucket ? [{
567
569
  name: 'complete_work',
568
- description: 'Close the loop on a finished branch: merges the open PR and transitions the Jira ticket to Done (or a named transition). Use when asked to "ship this", "close out FOO-123", "merge and close the ticket", or "done with this branch". Mirrors start_work.',
570
+ description: 'Close the loop on a finished branch: merges the open PR and transitions the Jira ticket to Done (or a named transition). Mirrors start_work.',
569
571
  inputSchema: {
570
572
  type: 'object',
571
573
  properties: {
package/dist/jira.js CHANGED
@@ -10,6 +10,14 @@ const EMOJI_RE = /\p{Extended_Pictographic}/u;
10
10
  function text(t) {
11
11
  return { content: [{ type: 'text', text: t }] };
12
12
  }
13
+ // Cap long free-text (e.g. issue descriptions) so a single verbose ticket does
14
+ // not flood the model's context. Returns the text untouched when within cap.
15
+ function capText(value, max) {
16
+ if (max <= 0 || value.length <= max)
17
+ return value;
18
+ const more = value.length - max;
19
+ return `${value.slice(0, max)}\n... (truncated, ${more} more chars — pass fullDescription=true for the rest)`;
20
+ }
13
21
  function pagination(total, startAt, count) {
14
22
  const end = startAt + count;
15
23
  return total > end ? ` (showing ${startAt + 1}–${end} of ${total}, use startAt=${end} for next page)` : '';
@@ -456,6 +464,7 @@ export class JiraClient {
456
464
  const includeComments = args.includeComments ?? true;
457
465
  const includeTransitions = args.includeTransitions ?? true;
458
466
  const includeSprint = args.includeSprint ?? true;
467
+ const descriptionCap = args.fullDescription ? 0 : args.descriptionMaxChars ?? 2000;
459
468
  const commentsMaxResults = args.commentsMaxResults ?? 10;
460
469
  const commentsStartAt = args.commentsStartAt ?? 0;
461
470
  const baseFields = 'summary,description,status,assignee,priority,issuetype,labels,components,parent,fixVersions,issuelinks,subtasks,attachment';
@@ -517,7 +526,7 @@ export class JiraClient {
517
526
  const names = (transitions?.transitions ?? []).map((t) => `${t.name} -> ${t.to.name}`);
518
527
  lines.push(`Transitions: ${names.length > 0 ? names.join(', ') : '(none)'}`);
519
528
  }
520
- lines.push('', 'Description:', f.description ?? '(no description)');
529
+ lines.push('', 'Description:', f.description ? capText(f.description, descriptionCap) : '(no description)');
521
530
  if (f.attachment?.length) {
522
531
  lines.push('', `Attachments: ${f.attachment.length}`);
523
532
  for (const att of f.attachment) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,14 +31,14 @@
31
31
  "dotenv": "^17.4.2",
32
32
  "ffmpeg-static": "^5.3.0",
33
33
  "ffprobe-static": "^3.1.0",
34
- "pdfjs-dist": "^5.6.205",
35
- "sharp": "^0.34.5",
34
+ "pdfjs-dist": "^5.7.284",
35
+ "sharp": "^0.35.1",
36
36
  "unpdf": "^1.6.2"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^25.6.0",
40
- "npm-check-updates": "^21.0.2",
41
- "tsx": "^4.21.0",
39
+ "@types/node": "^25.9.3",
40
+ "npm-check-updates": "^21.0.3",
41
+ "tsx": "^4.22.4",
42
42
  "typescript": "^6.0.3"
43
43
  },
44
44
  "engines": {