@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 +3 -0
- package/dist/context.js +53 -9
- package/dist/index.js +60 -2
- package/dist/jira.js +3 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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;
|