@stubbedev/atlassian-mcp 0.2.11 → 0.2.12

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
@@ -371,6 +371,24 @@ export class BitbucketClient {
371
371
  });
372
372
  return text(`${data.values.length} user(s)${pageHint(data)}:\n${lines.join('\n')}`);
373
373
  }
374
+ async searchUsersRaw(args) {
375
+ const params = new URLSearchParams();
376
+ if (args.query)
377
+ params.set('filter', args.query);
378
+ params.set('limit', String(args.limit ?? 50));
379
+ let path;
380
+ if (args.projectKey && args.repoSlug) {
381
+ path = `${this.rp(args.projectKey, args.repoSlug)}/permissions/users?${params}`;
382
+ }
383
+ else if (args.projectKey) {
384
+ path = `/projects/${encodeURIComponent(args.projectKey)}/permissions/users?${params}`;
385
+ }
386
+ else {
387
+ path = `/users?${params}`;
388
+ }
389
+ const data = await this.request('GET', path);
390
+ return (data?.values ?? []).map((entry) => entry.user ?? entry);
391
+ }
374
392
  async listPullRequests(args) {
375
393
  const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
376
394
  const { state = 'OPEN', fromBranch, text: searchText, limit = 25, start = 0 } = args;
@@ -761,6 +779,16 @@ export class BitbucketClient {
761
779
  }
762
780
  return text(content);
763
781
  }
782
+ async fetchFileText(projectKey, repoSlug, filePath) {
783
+ try {
784
+ const encoded = filePath.split('/').map(encodeURIComponent).join('/');
785
+ const content = await this.requestText(`${this.rp(projectKey, repoSlug)}/raw/${encoded}`);
786
+ return content;
787
+ }
788
+ catch {
789
+ return null;
790
+ }
791
+ }
764
792
  async getPrComments(args) {
765
793
  const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
766
794
  const limit = args.limit ?? 50;
package/dist/index.js CHANGED
@@ -132,18 +132,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
132
132
  ...(jira ? [
133
133
  {
134
134
  name: 'start_work',
135
- description: 'Start working on a Jira ticket: fetches the ticket, creates a local git branch with an auto-generated name (e.g. feature/FOO-123-add-payment-gateway), and optionally transitions the ticket. Use when told "make a branch for FOO-123", "start working on this ticket", "check out a branch for this issue", or "begin work on FOO-123".',
135
+ 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.',
136
136
  inputSchema: {
137
137
  type: 'object',
138
138
  properties: {
139
- issueKey: { type: 'string', description: 'Jira issue key, e.g. FOO-123' },
139
+ issueKey: { type: 'string', description: 'Jira issue key, e.g. FOO-123 (provide this OR query)' },
140
+ query: { type: 'string', description: 'Free-text search when issueKey is unknown — shows a picker if multiple tickets match' },
140
141
  repoPath: { type: 'string', description: 'Local repo path (defaults to cwd)' },
141
142
  baseBranch: { type: 'string', description: 'Branch to base off (default: master)' },
142
143
  branchName: { type: 'string', description: 'Override the generated branch name' },
143
144
  transitionName: { type: 'string', description: 'Jira transition to apply, e.g. "In Progress" (optional)' },
144
145
  push: { type: 'boolean', description: 'Push branch to remote after creation (default false)', default: false },
145
146
  },
146
- required: ['issueKey'],
147
147
  },
148
148
  },
149
149
  {
@@ -332,6 +332,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
332
332
  fromBranch: { type: 'string', description: 'Source branch (defaults to current branch)' },
333
333
  toBranch: { type: 'string', description: 'Target branch (default: master)' },
334
334
  reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer usernames. Use bitbucket_search resource=users to look up valid usernames before setting this.' },
335
+ pickReviewers: { type: 'boolean', description: 'Show an interactive reviewer picker before creating the PR (lists users with repo access)' },
335
336
  },
336
337
  required: ['title'],
337
338
  },
@@ -462,8 +463,69 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
462
463
  if (!jira)
463
464
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
464
465
  const a = args;
465
- const fields = await jira.getIssueFields(a.issueKey);
466
- const branchName = a.branchName ?? slugifyBranchName(a.issueKey, fields.summary, fields.type);
466
+ if (!a.issueKey && !a.query) {
467
+ throw new Error('Provide issueKey (e.g. FOO-123) or query (free-text search).');
468
+ }
469
+ // Resolve issueKey from free-text query if not provided directly
470
+ let issueKey = a.issueKey;
471
+ if (!issueKey && a.query) {
472
+ const candidates = await jira.findIssues(a.query, 10);
473
+ if (candidates.length === 0) {
474
+ return { content: [{ type: 'text', text: `No Jira tickets found for: "${a.query}"` }] };
475
+ }
476
+ if (candidates.length === 1) {
477
+ issueKey = candidates[0].key;
478
+ }
479
+ else {
480
+ // Multiple matches — present a picker
481
+ const pickerMessage = [
482
+ `Found ${candidates.length} tickets matching "${a.query}". Which one do you want to work on?`,
483
+ ...candidates.map((c, i) => `${i + 1}. [${c.key}] ${c.summary} (${c.status})`),
484
+ ].join('\n');
485
+ try {
486
+ const pickerResult = await server.elicitInput({
487
+ message: pickerMessage,
488
+ requestedSchema: {
489
+ type: 'object',
490
+ properties: {
491
+ ticket: {
492
+ type: 'string',
493
+ title: 'Select a ticket',
494
+ oneOf: [
495
+ ...candidates.map((c) => ({ const: c.key, title: `[${c.key}] ${c.summary}` })),
496
+ { const: '__cancel__', title: 'Cancel' },
497
+ ],
498
+ },
499
+ },
500
+ required: ['ticket'],
501
+ },
502
+ });
503
+ if (pickerResult.action === 'cancel' ||
504
+ pickerResult.action === 'decline' ||
505
+ pickerResult.content?.ticket === '__cancel__') {
506
+ return { content: [{ type: 'text', text: 'Cancelled.' }] };
507
+ }
508
+ issueKey = pickerResult.content?.ticket;
509
+ if (!issueKey || issueKey === '__cancel__') {
510
+ return { content: [{ type: 'text', text: 'Cancelled.' }] };
511
+ }
512
+ }
513
+ catch {
514
+ // Client doesn't support elicitation — list options and ask caller to retry with issueKey
515
+ const list = candidates.map((c) => ` • ${c.key} — ${c.summary} (${c.status})`).join('\n');
516
+ return {
517
+ content: [{
518
+ type: 'text',
519
+ text: `Found ${candidates.length} tickets matching "${a.query}":\n${list}\n\nRe-run start_work with the desired issueKey.`,
520
+ }],
521
+ };
522
+ }
523
+ }
524
+ }
525
+ if (!issueKey)
526
+ throw new Error('Could not resolve issue key.');
527
+ const fields = await jira.getIssueFields(issueKey);
528
+ const branchName = a.branchName ?? slugifyBranchName(issueKey, fields.summary, fields.type);
467
529
  const repoPath = a.repoPath ?? process.cwd();
468
530
  // Check if branch already exists on remote before creating
469
531
  const remote = checkRemoteBranch(branchName, repoPath);
@@ -475,7 +537,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
475
537
  const contextLines = [authorLine, commitLine].filter(Boolean).join('\n');
476
538
  const message = [
477
539
  `Branch "${branchName}" already exists on remote.`,
478
- `Ticket: ${a.issueKey} — ${fields.summary}`,
540
+ `Ticket: ${issueKey} — ${fields.summary}`,
479
541
  contextLines,
480
542
  ].filter(Boolean).join('\n');
481
543
  try {
@@ -509,7 +571,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
509
571
  if (chosen === 'cancel') {
510
572
  return { content: [{ type: 'text', text: 'Cancelled.' }] };
511
573
  }
512
- // new_name — instruct the model to re-run with a custom name
513
574
  return {
514
575
  content: [{
515
576
  type: 'text',
@@ -517,11 +578,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
517
578
  }],
518
579
  };
519
580
  }
520
- // Fallback: unknown action
521
581
  return { content: [{ type: 'text', text: 'Cancelled.' }] };
522
582
  }
523
583
  catch {
524
- // Client doesn't support elicitation — fall back to informational text
525
584
  return {
526
585
  content: [{
527
586
  type: 'text',
@@ -543,21 +602,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
543
602
  push: a.push ?? false,
544
603
  });
545
604
  const lines = [
546
- `Ticket: ${a.issueKey} — ${fields.summary}`,
605
+ `Ticket: ${issueKey} — ${fields.summary}`,
547
606
  `Status: ${fields.status}`,
548
607
  branchResult.content[0].text,
549
608
  ];
550
609
  if (a.transitionName) {
551
610
  try {
552
- await jira.mutateIssue({ issueKey: a.issueKey, transitionName: a.transitionName });
611
+ await jira.mutateIssue({ issueKey, transitionName: a.transitionName });
553
612
  lines.push(`Jira: transitioned → ${a.transitionName}`);
554
613
  }
555
614
  catch (err) {
556
615
  lines.push(`Jira: could not transition — ${err.message}`);
557
616
  }
558
617
  }
559
- if (bitbucket)
560
- lines.push(``, `Next: push commits then use bitbucket_mutate to open a PR.`);
618
+ // Fetch README from Bitbucket for project conventions
619
+ if (bitbucket) {
620
+ try {
621
+ const remoteUrl = (() => {
622
+ try {
623
+ return execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: repoPath, encoding: 'utf-8' }).trim();
624
+ }
625
+ catch {
626
+ return '';
627
+ }
628
+ })();
629
+ const parsed = parseBitbucketRemote(remoteUrl);
630
+ if (parsed) {
631
+ const readme = await bitbucket.fetchFileText(parsed.projectKey, parsed.repoSlug, 'README.md');
632
+ if (readme) {
633
+ const maxLen = 4000;
634
+ const truncated = readme.length > maxLen ? readme.slice(0, maxLen) + '\n... (truncated)' : readme;
635
+ lines.push('');
636
+ lines.push('Project conventions (from README.md):');
637
+ lines.push('────────────────────────────────────');
638
+ lines.push(truncated);
639
+ lines.push('────────────────────────────────────');
640
+ lines.push('Follow the conventions above when writing commit messages and the PR description.');
641
+ }
642
+ }
643
+ }
644
+ catch { /* README fetch is best-effort */ }
645
+ lines.push('');
646
+ lines.push('Next steps:');
647
+ lines.push(' 1. Make your changes and commit following the project conventions.');
648
+ lines.push(' 2. Use bitbucket_mutate (create) to open a PR — the Jira summary and ticket key make a good title/description starting point.');
649
+ lines.push(' 3. Add reviewers: bitbucket_search resource=users to find colleagues, or pass pickReviewers=true in create to get an interactive picker.');
650
+ }
561
651
  return { content: [{ type: 'text', text: lines.join('\n') }] };
562
652
  }
563
653
  // Jira
@@ -636,6 +726,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
636
726
  return await bitbucket.declinePr({ ...a, message: a.declineMessage });
637
727
  if (action === 'merge')
638
728
  return await bitbucket.mergePr({ ...a, message: a.mergeMessage, mergeStrategy: a.mergeStrategy });
729
+ // Handle interactive reviewer picker for PR creation
730
+ const createArgs = a.create;
731
+ if (createArgs?.pickReviewers && !a.prId) {
732
+ const projectKey = a.projectKey;
733
+ const repoSlug = a.repoSlug;
734
+ const users = await bitbucket.searchUsersRaw({ projectKey, repoSlug, limit: 30 });
735
+ if (users.length > 0) {
736
+ // Build boolean checkbox schema — one field per user
737
+ // key must be safe for JSON schema property names
738
+ const toSchemaKey = (uname) => uname.replace(/[^a-zA-Z0-9]/g, '_');
739
+ const userMap = new Map(); // schemaKey -> username
740
+ const properties = {};
741
+ for (const u of users) {
742
+ const key = toSchemaKey(u.name);
743
+ userMap.set(key, u.name);
744
+ properties[key] = { type: 'boolean', title: `${u.displayName} (${u.name})` };
745
+ }
746
+ try {
747
+ const pickerResult = await server.elicitInput({
748
+ message: 'Select reviewers to add to this PR:',
749
+ requestedSchema: { type: 'object', properties },
750
+ });
751
+ if (pickerResult.action === 'accept' && pickerResult.content) {
752
+ const selected = [];
753
+ for (const [key, username] of userMap) {
754
+ if (pickerResult.content[key] === true)
755
+ selected.push(username);
756
+ }
757
+ createArgs.reviewers = selected;
758
+ }
759
+ }
760
+ catch {
761
+ // elicitation not supported — proceed without reviewer picker
762
+ }
763
+ }
764
+ }
639
765
  return await bitbucket.mutatePullRequest(a);
640
766
  }
641
767
  case 'bitbucket_comment': {
package/dist/jira.js CHANGED
@@ -285,6 +285,22 @@ export class JiraClient {
285
285
  const page = pagination(data.total, startAt, data.issues.length);
286
286
  return text(`Found ${data.total} issues${page}:\n${lines.join('\n')}`);
287
287
  }
288
+ async findIssues(query, maxResults = 10) {
289
+ const jql = buildJQL({ query });
290
+ const params = new URLSearchParams({
291
+ jql,
292
+ maxResults: String(maxResults),
293
+ startAt: '0',
294
+ fields: 'summary,status,issuetype',
295
+ });
296
+ const data = await this.request('GET', `/search?${params}`);
297
+ return (data?.issues ?? []).map((i) => ({
298
+ key: i.key,
299
+ summary: i.fields.summary,
300
+ status: i.fields.status.name,
301
+ type: i.fields.issuetype.name,
302
+ }));
303
+ }
288
304
  async myIssues(args) {
289
305
  return this.searchIssues({
290
306
  jql: 'assignee = currentUser() ORDER BY updated DESC',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
5
  "license": "MIT",
6
6
  "type": "module",