@stubbedev/atlassian-mcp 0.3.0 → 0.3.1

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
@@ -237,6 +237,9 @@ export class BitbucketClient {
237
237
  this.currentUsernameCache = username;
238
238
  return username;
239
239
  }
240
+ async whoami() {
241
+ return this.getCurrentUsername();
242
+ }
240
243
  /** Returns a URL-safe `/projects/.../repos/...` prefix for REST paths. */
241
244
  rp(projectKey, repoSlug) {
242
245
  return `/projects/${encodeURIComponent(projectKey)}/repos/${encodeURIComponent(repoSlug)}`;
package/dist/context.js CHANGED
@@ -9,6 +9,27 @@ function safeExec(cmd, cwd) {
9
9
  return '';
10
10
  }
11
11
  }
12
+ /** Top recent committers from the last `lookback` commits, ranked by count. */
13
+ export function getTopCommitters(repoPath, lookback = 50, top = 5) {
14
+ const raw = safeExec(`git log -n ${lookback} --format=%aN%x09%aE`, repoPath);
15
+ if (!raw)
16
+ return [];
17
+ const counts = new Map();
18
+ for (const line of raw.split('\n')) {
19
+ const [name, email] = line.split('\t');
20
+ if (!name)
21
+ continue;
22
+ const key = (email || name).toLowerCase();
23
+ const existing = counts.get(key);
24
+ if (existing)
25
+ existing.commits++;
26
+ else
27
+ counts.set(key, { name, email: email ?? '', commits: 1 });
28
+ }
29
+ return [...counts.values()]
30
+ .sort((a, b) => b.commits - a.commits)
31
+ .slice(0, top);
32
+ }
12
33
  /**
13
34
  * Unified developer context: git state + linked Jira issues + open PR for current branch.
14
35
  * Either jira or bitbucket may be null when only one product is configured.
@@ -20,6 +41,8 @@ export async function getDevContext(args, jira, bitbucket) {
20
41
  const remote = safeExec('git remote get-url origin', repoPath) || '(no remote)';
21
42
  const recentCommits = safeExec('git log --oneline -5', repoPath) || '(none)';
22
43
  const status = safeExec('git status --short', repoPath) || '(clean)';
44
+ const committers = getTopCommitters(repoPath, 50, 5);
45
+ const parsed = bitbucket?.isRemoteForThisInstance(remote) ? parseBitbucketRemote(remote) : null;
23
46
  // Upstream ahead/behind
24
47
  const upstream = safeExec('git rev-parse --abbrev-ref @{u}', repoPath);
25
48
  let upstreamLine = '';
@@ -35,18 +58,40 @@ export async function getDevContext(args, jira, bitbucket) {
35
58
  upstreamLine = `${upstream}${parts.length ? ` (${parts.join(', ')})` : ' (up to date)'}`;
36
59
  }
37
60
  }
38
- sections.push([
61
+ // Identity — best-effort, parallel
62
+ const [jiraMe, bbMe] = await Promise.all([
63
+ jira ? jira.whoami().catch(() => null) : Promise.resolve(null),
64
+ bitbucket ? bitbucket.whoami().catch(() => null) : Promise.resolve(null),
65
+ ]);
66
+ const youParts = [];
67
+ if (jiraMe)
68
+ youParts.push(`Jira ${jiraMe.name ?? jiraMe.key ?? '(unknown)'}${jiraMe.displayName ? ` "${jiraMe.displayName}"` : ''}`);
69
+ if (bbMe)
70
+ youParts.push(`Bitbucket ${bbMe}`);
71
+ const headerLines = [
39
72
  `Repository: ${repoPath}`,
40
73
  `Branch: ${branch}`,
41
74
  ...(upstreamLine ? [`Upstream: ${upstreamLine}`] : []),
42
75
  `Remote: ${remote}`,
43
- '',
44
- 'Recent commits:',
45
- recentCommits,
46
- '',
47
- 'Working tree:',
48
- status,
49
- ].join('\n'));
76
+ ...(parsed ? [`Bitbucket: ${parsed.projectKey}/${parsed.repoSlug}`] : []),
77
+ ...(youParts.length ? [`You: ${youParts.join(' · ')}`] : []),
78
+ ];
79
+ if (committers.length) {
80
+ headerLines.push('');
81
+ headerLines.push('Top recent committers (last 50):');
82
+ for (const c of committers) {
83
+ const ident = c.email ? `${c.name} <${c.email}>` : c.name;
84
+ headerLines.push(` ${c.commits.toString().padStart(3)} — ${ident}`);
85
+ }
86
+ headerLines.push(' (look up usernames via jira_search resource=users / bitbucket_search resource=users — do NOT shell out to git/gh/bb)');
87
+ }
88
+ headerLines.push('');
89
+ headerLines.push('Recent commits:');
90
+ headerLines.push(recentCommits);
91
+ headerLines.push('');
92
+ headerLines.push('Working tree:');
93
+ headerLines.push(status);
94
+ sections.push(headerLines.join('\n'));
50
95
  // Jira — fetch overview for any tickets referenced in the branch name (parallel)
51
96
  const jiraKeys = jira ? [...new Set(branch.match(JIRA_KEY_RE) ?? [])] : [];
52
97
  const jiraResults = await Promise.all(jiraKeys.map(async (key) => {
@@ -66,7 +111,6 @@ export async function getDevContext(args, jira, bitbucket) {
66
111
  }));
67
112
  sections.push(...jiraResults);
68
113
  // Bitbucket — find the open PR for this branch (only if remote points to this instance)
69
- const parsed = bitbucket?.isRemoteForThisInstance(remote) ? parseBitbucketRemote(remote) : null;
70
114
  if (parsed) {
71
115
  try {
72
116
  const pr = await bitbucket.findOpenPrForBranch(parsed.projectKey, parsed.repoSlug, branch);
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { loadConfig } from './config.js';
12
12
  import { JiraClient } from './jira.js';
13
13
  import { BitbucketClient, parseBitbucketRemote } from './bitbucket.js';
14
14
  import { getContext, getDiff, createBranch, checkRemoteBranch, checkoutRemoteBranch } from './git.js';
15
- import { getDevContext } from './context.js';
15
+ import { getDevContext, getTopCommitters } from './context.js';
16
16
  function currentGitRemote() {
17
17
  try {
18
18
  return execFileSync('git', ['remote', 'get-url', 'origin'], { encoding: 'utf-8' }).trim();
@@ -21,6 +21,14 @@ function currentGitRemote() {
21
21
  return '';
22
22
  }
23
23
  }
24
+ function currentGitBranch() {
25
+ try {
26
+ return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf-8' }).trim();
27
+ }
28
+ catch {
29
+ return '';
30
+ }
31
+ }
24
32
  function remoteMatchesBitbucketInstance(remote, bitbucketUrl) {
25
33
  if (!remote)
26
34
  return false;
@@ -39,7 +47,57 @@ const bitbucket = (config.bitbucket && remoteMatchesBitbucketInstance(_remote, c
39
47
  if (config.bitbucket && !bitbucket) {
40
48
  console.error(`[atlassian-mcp] Bitbucket configured but remote "${_remote || '(none)'}" does not match ${config.bitbucket.url} — Bitbucket tools disabled for this repo.`);
41
49
  }
42
- const server = new Server({ name: 'atlassian-mcp', version: _pkg.version }, { capabilities: { tools: {} } });
50
+ async function buildInstructions() {
51
+ const branch = currentGitBranch();
52
+ const isGitRepo = branch !== '';
53
+ const jiraKeys = isGitRepo ? [...new Set(branch.match(/\b[A-Z][A-Z0-9]+-\d+\b/g) ?? [])] : [];
54
+ const parsed = bitbucket && _remote ? parseBitbucketRemote(_remote) : null;
55
+ const committers = isGitRepo ? getTopCommitters(process.cwd(), 50, 5) : [];
56
+ const [jiraMe, bbMe] = await Promise.all([
57
+ jira ? jira.whoami().catch(() => null) : Promise.resolve(null),
58
+ bitbucket ? bitbucket.whoami().catch(() => null) : Promise.resolve(null),
59
+ ]);
60
+ const lines = [];
61
+ lines.push('# atlassian-mcp');
62
+ lines.push('');
63
+ lines.push('Self-hosted Jira + Bitbucket Server tooling. Prefer these tools over shelling out to `git log`, `gh`, or any `bitbucket`/`bb` CLI for anything that touches tickets, PRs, reviewers, comments, or user lookups.');
64
+ lines.push('');
65
+ lines.push('## Configured services');
66
+ lines.push(`- Jira: ${jira ? config.jira.url : '(not configured)'}${jiraMe ? ` — you are ${jiraMe.name ?? jiraMe.key ?? '?'}${jiraMe.displayName ? ` "${jiraMe.displayName}"` : ''}` : ''}`);
67
+ lines.push(`- Bitbucket: ${bitbucket ? config.bitbucket.url : (config.bitbucket ? `${config.bitbucket.url} — DISABLED for this cwd (remote does not match)` : '(not configured)')}${bbMe ? ` — you are ${bbMe}` : ''}`);
68
+ lines.push('');
69
+ if (isGitRepo) {
70
+ lines.push('## Current repo');
71
+ lines.push(`- Branch: ${branch} (may have changed since startup — re-run \`get_dev_context\` to refresh)`);
72
+ lines.push(`- Remote: ${_remote || '(none)'}`);
73
+ if (parsed)
74
+ lines.push(`- Bitbucket repo: ${parsed.projectKey}/${parsed.repoSlug}`);
75
+ if (jiraKeys.length)
76
+ lines.push(`- Jira keys in branch: ${jiraKeys.join(', ')}`);
77
+ }
78
+ else {
79
+ lines.push('## Current repo');
80
+ lines.push('- Not a git repository.');
81
+ }
82
+ if (committers.length) {
83
+ lines.push('');
84
+ lines.push('## Recent committers in this repo (last 50 commits)');
85
+ for (const c of committers) {
86
+ const ident = c.email ? `${c.name} <${c.email}>` : c.name;
87
+ lines.push(`- ${c.commits}× ${ident}`);
88
+ }
89
+ }
90
+ lines.push('');
91
+ lines.push('## Use these tools — do NOT shell out');
92
+ lines.push('- "What am I working on / what\'s the status / show me the context" → call `get_dev_context` first. It returns branch state, linked Jira tickets, the open PR, and reviewer status in one shot.');
93
+ lines.push('- Looking up a person\'s username (for reviewers, assignees, mentions) → ALWAYS use `bitbucket_search resource=users` or `jira_search resource=users`. NEVER use `git log`/`git shortlog`/`gh api`/`bb`/any bitbucket CLI to discover who someone is — those return commit-author strings, not Bitbucket/Jira usernames, and the wrong identifier breaks reviewer assignment.');
94
+ lines.push('- Reading a Jira ticket → `jira_get` (single) or `jira_search` (many). Mutating → `jira_mutate`.');
95
+ lines.push('- Reading a PR → `bitbucket_get_pr`. Creating/updating/merging → `bitbucket_mutate`. Commenting → `bitbucket_comment`.');
96
+ lines.push('- Starting work on a ticket (branch + status transition + README) → `start_work`. Closing it (merge + transition) → `complete_work`.');
97
+ return lines.join('\n');
98
+ }
99
+ const _instructions = await buildInstructions();
100
+ const server = new Server({ name: 'atlassian-mcp', version: _pkg.version }, { capabilities: { tools: {} }, instructions: _instructions });
43
101
  server.onerror = (error) => console.error('[MCP Error]', error);
44
102
  function normalizeBitbucketArgs(args) {
45
103
  const src = (args && typeof args === 'object') ? args : {};
package/dist/jira.js CHANGED
@@ -149,6 +149,9 @@ export class JiraClient {
149
149
  this.currentUserCache = me;
150
150
  return me;
151
151
  }
152
+ async whoami() {
153
+ return this.getCurrentUser();
154
+ }
152
155
  async getIssueLinkingEnabled() {
153
156
  if (this.issueLinkingEnabled !== undefined)
154
157
  return this.issueLinkingEnabled;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket",
5
5
  "license": "MIT",
6
6
  "type": "module",