@stubbedev/atlassian-mcp 0.2.9 → 0.2.11

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
@@ -340,6 +340,37 @@ export class BitbucketClient {
340
340
  const lines = data.values.map((r, i) => `${start + i + 1}. ${r.project.key}/${r.slug} — ${r.name}`);
341
341
  return text(`${data.values.length} repo(s)${pageHint(data)}:\n${lines.join('\n')}`);
342
342
  }
343
+ async searchUsers(args) {
344
+ const params = new URLSearchParams();
345
+ if (args.query)
346
+ params.set('filter', args.query);
347
+ params.set('limit', String(args.limit ?? 25));
348
+ if (args.start)
349
+ params.set('start', String(args.start));
350
+ let path;
351
+ if (args.projectKey && args.repoSlug) {
352
+ path = `${this.rp(args.projectKey, args.repoSlug)}/permissions/users?${params}`;
353
+ }
354
+ else if (args.projectKey) {
355
+ path = `/projects/${encodeURIComponent(args.projectKey)}/permissions/users?${params}`;
356
+ }
357
+ else {
358
+ path = `/users?${params}`;
359
+ }
360
+ const data = await this.request('GET', path);
361
+ if (!data || data.values.length === 0)
362
+ return text('No users found.');
363
+ const lines = data.values.map((entry, i) => {
364
+ const user = entry.user ?? entry;
365
+ const parts = [`${i + 1}. ${user.displayName} (${user.name})`];
366
+ if (user.emailAddress)
367
+ parts.push(`— ${user.emailAddress}`);
368
+ if (user.active === false)
369
+ parts.push('[inactive]');
370
+ return parts.join(' ');
371
+ });
372
+ return text(`${data.values.length} user(s)${pageHint(data)}:\n${lines.join('\n')}`);
373
+ }
343
374
  async listPullRequests(args) {
344
375
  const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
345
376
  const { state = 'OPEN', fromBranch, text: searchText, limit = 25, start = 0 } = args;
@@ -972,7 +1003,7 @@ export class BitbucketClient {
972
1003
  return text(`${data.values.length} task(s) on PR #${args.prId} (${open} open)${pageHint(data)}:\n${lines.join('\n')}`);
973
1004
  }
974
1005
  async mutatePrTask(args) {
975
- const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
1006
+ this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
976
1007
  if (args.action === 'create') {
977
1008
  if (!args.text)
978
1009
  throw new Error('text is required to create a task.');
package/dist/index.js CHANGED
@@ -263,17 +263,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
263
263
  ] : []),
264
264
  ...(bitbucket ? [{
265
265
  name: 'bitbucket_search',
266
- 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',
266
+ 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.',
267
267
  inputSchema: {
268
268
  type: 'object',
269
269
  properties: {
270
- resource: { type: 'string', enum: ['pull_requests', 'repos', 'branches'], description: 'What to search (default: pull_requests)' },
270
+ resource: { type: 'string', enum: ['pull_requests', 'repos', 'branches', 'users'], description: 'What to search (default: pull_requests)' },
271
271
  mine: { type: 'boolean', description: 'Return your own PRs by role (resource=pull_requests only)' },
272
272
  role: { type: 'string', enum: ['author', 'reviewer', 'participant'], description: 'Your role filter when mine=true' },
273
273
  projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG"' },
274
274
  project: { type: 'string', description: 'Alias for projectKey' },
275
275
  repoSlug: { type: 'string', description: 'Repository slug' },
276
276
  repo: { type: 'string', description: 'Alias for repoSlug' },
277
+ query: { type: 'string', description: 'Name or email filter (resource=users only)' },
277
278
  state: { type: 'string', enum: ['OPEN', 'MERGED', 'DECLINED'], description: 'PR state filter (default OPEN)' },
278
279
  fromBranch: { type: 'string', description: 'Filter PRs from this source branch' },
279
280
  text: { type: 'string', description: 'Filter PRs by title/description text' },
@@ -285,7 +286,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
285
286
  },
286
287
  {
287
288
  name: 'bitbucket_get_pr',
288
- description: 'Full details for one PR: metadata, commits, open comments, blockers, and optional diff. 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. For full file context during review, follow up with bitbucket_get_file. 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.',
289
+ description: 'Full details for one PR: metadata, commits, open comments, blockers, and optional diff. 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. 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.',
289
290
  inputSchema: {
290
291
  type: 'object',
291
292
  properties: {
@@ -330,7 +331,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
330
331
  description: { type: 'string', description: 'PR description (optional)' },
331
332
  fromBranch: { type: 'string', description: 'Source branch (defaults to current branch)' },
332
333
  toBranch: { type: 'string', description: 'Target branch (default: master)' },
333
- reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer usernames (optional)' },
334
+ reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer usernames. Use bitbucket_search resource=users to look up valid usernames before setting this.' },
334
335
  },
335
336
  required: ['title'],
336
337
  },
@@ -340,7 +341,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
340
341
  title: { type: 'string', description: 'Updated PR title (optional)' },
341
342
  description: { type: 'string', description: 'Updated description, or empty string to clear (optional)' },
342
343
  toBranch: { type: 'string', description: 'Updated target branch (optional)' },
343
- reviewers: { type: 'array', items: { type: 'string' }, description: 'Updated reviewer usernames. Empty array clears reviewers.' },
344
+ reviewers: { type: 'array', items: { type: 'string' }, description: 'Updated reviewer usernames. Empty array clears reviewers. Use bitbucket_search resource=users to look up valid usernames before setting this.' },
344
345
  },
345
346
  },
346
347
  },
@@ -377,7 +378,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
377
378
  },
378
379
  {
379
380
  name: 'bitbucket_get_file',
380
- description: 'Raw file content from Bitbucket at a branch, tag, or commit. Use during PR reviews to get full-file context without relying on local checkout.',
381
+ description: 'Raw file content from Bitbucket at a branch, tag, or commit. CRITICAL: if the PR branch being reviewed is NOT the currently checked-out local branch, ALL additional file context for that review MUST come from this tool — never from local Read, git_get_diff, or any tool that reads local disk. Pass the PR source branch as ref.',
381
382
  inputSchema: {
382
383
  type: 'object',
383
384
  properties: {
@@ -611,6 +612,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
611
612
  return await bitbucket.listRepos(a);
612
613
  if (resource === 'branches')
613
614
  return await bitbucket.getBranches(a);
615
+ if (resource === 'users')
616
+ return await bitbucket.searchUsers({ projectKey: a.projectKey, repoSlug: a.repoSlug, query: a.query, limit: a.limit, start: a.start });
614
617
  // pull_requests (default)
615
618
  if (a.mine)
616
619
  return await bitbucket.myPrs({ limit: a.limit, start: a.start, role: a.role });
package/dist/jira.js CHANGED
@@ -95,6 +95,7 @@ export class JiraClient {
95
95
  headers;
96
96
  currentUserCache;
97
97
  projectsCache;
98
+ issueLinkingEnabled;
98
99
  constructor(baseUrl, token) {
99
100
  this.baseUrl = baseUrl.replace(/\/$/, '');
100
101
  this.headers = {
@@ -147,6 +148,13 @@ export class JiraClient {
147
148
  this.currentUserCache = me;
148
149
  return me;
149
150
  }
151
+ async getIssueLinkingEnabled() {
152
+ if (this.issueLinkingEnabled !== undefined)
153
+ return this.issueLinkingEnabled;
154
+ const config = await this.request('GET', '/configuration');
155
+ this.issueLinkingEnabled = config?.issueLinkingEnabled ?? false;
156
+ return this.issueLinkingEnabled;
157
+ }
150
158
  async assertOwnComment(comment) {
151
159
  const me = await this.getCurrentUser();
152
160
  const commentAuthorName = this.normalizeIdentity(comment.author.name);
@@ -602,14 +610,20 @@ export class JiraClient {
602
610
  await this.request('POST', `/issue/${encodeURIComponent(issueKey)}/comment`, { body: validateCommentBody(args.comment) });
603
611
  actions.push('added comment');
604
612
  }
613
+ const warnings = [];
605
614
  if (args.link) {
606
- const dir = args.link.direction ?? 'outward';
607
- await this.request('POST', '/issueLink', {
608
- type: { name: args.link.linkType },
609
- outwardIssue: { key: dir === 'outward' ? issueKey : args.link.targetIssueKey },
610
- inwardIssue: { key: dir === 'outward' ? args.link.targetIssueKey : issueKey },
611
- });
612
- actions.push(`linked ${args.link.linkType} → ${args.link.targetIssueKey}`);
615
+ if (!(await this.getIssueLinkingEnabled())) {
616
+ warnings.push(`issue linking is disabled in this Jira instance — add the link manually`);
617
+ }
618
+ else {
619
+ const dir = args.link.direction ?? 'outward';
620
+ await this.request('POST', '/issueLink', {
621
+ type: { name: args.link.linkType },
622
+ outwardIssue: { key: dir === 'outward' ? issueKey : args.link.targetIssueKey },
623
+ inwardIssue: { key: dir === 'outward' ? args.link.targetIssueKey : issueKey },
624
+ });
625
+ actions.push(`linked ${args.link.linkType} → ${args.link.targetIssueKey}`);
626
+ }
613
627
  }
614
628
  if (args.worklog) {
615
629
  const wBody = { timeSpent: args.worklog.timeSpent };
@@ -623,7 +637,10 @@ export class JiraClient {
623
637
  if (actions.length === 0) {
624
638
  return text('Nothing to mutate.');
625
639
  }
626
- return text(`Mutated ${issueKey}: ${actions.join(', ')}.\n${this.issueUrl(issueKey)}`);
640
+ const parts = [`Mutated ${issueKey}: ${actions.join(', ')}.`, this.issueUrl(issueKey)];
641
+ if (warnings.length)
642
+ parts.push(`Warnings: ${warnings.join('; ')}`);
643
+ return text(parts.join('\n'));
627
644
  }
628
645
  async getComments(args) {
629
646
  const { issueKey, maxResults = 50, startAt = 0 } = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
5
  "license": "MIT",
6
6
  "type": "module",