@stubbedev/atlassian-mcp 0.4.5 → 0.5.0

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/config.js DELETED
@@ -1,62 +0,0 @@
1
- import { readFileSync, existsSync } from 'fs';
2
- import { homedir } from 'os';
3
- import { join, resolve } from 'path';
4
- function readJsonFile(filePath) {
5
- try {
6
- if (!existsSync(filePath))
7
- return null;
8
- return JSON.parse(readFileSync(filePath, 'utf-8'));
9
- }
10
- catch {
11
- return null;
12
- }
13
- }
14
- function getConfigPath() {
15
- const configArgIndex = process.argv.indexOf('--config');
16
- if (configArgIndex !== -1 && process.argv[configArgIndex + 1]) {
17
- return resolve(process.argv[configArgIndex + 1]);
18
- }
19
- if (process.env.ATLASSIAN_MCP_CONFIG) {
20
- return resolve(process.env.ATLASSIAN_MCP_CONFIG);
21
- }
22
- const homeConfig = join(homedir(), '.atlassian-mcp.json');
23
- if (existsSync(homeConfig))
24
- return homeConfig;
25
- const cwdConfig = join(process.cwd(), '.atlassian-mcp.json');
26
- if (existsSync(cwdConfig))
27
- return cwdConfig;
28
- return null;
29
- }
30
- export function loadConfig() {
31
- const configPath = getConfigPath();
32
- const file = configPath ? readJsonFile(configPath) : null;
33
- const jiraUrl = file?.jira?.url ?? process.env.JIRA_URL ?? '';
34
- const jiraToken = file?.jira?.token ?? process.env.JIRA_ACCESS_TOKEN ?? '';
35
- const bitbucketUrl = file?.bitbucket?.url ?? process.env.BITBUCKET_URL ?? '';
36
- const bitbucketToken = file?.bitbucket?.token ?? process.env.BITBUCKET_ACCESS_TOKEN ?? '';
37
- const config = {};
38
- if (jiraUrl && jiraToken) {
39
- config.jira = { url: jiraUrl, token: jiraToken };
40
- }
41
- else if (jiraUrl || jiraToken) {
42
- // Partially configured — log which piece is missing so the user can fix it
43
- const missing = [];
44
- if (!jiraUrl)
45
- missing.push('jira.url (or JIRA_URL)');
46
- if (!jiraToken)
47
- missing.push('jira.token (or JIRA_ACCESS_TOKEN)');
48
- console.error(`[atlassian-mcp] Jira disabled: missing ${missing.join(', ')}`);
49
- }
50
- if (bitbucketUrl && bitbucketToken) {
51
- config.bitbucket = { url: bitbucketUrl, token: bitbucketToken };
52
- }
53
- else if (bitbucketUrl || bitbucketToken) {
54
- const missing = [];
55
- if (!bitbucketUrl)
56
- missing.push('bitbucket.url (or BITBUCKET_URL)');
57
- if (!bitbucketToken)
58
- missing.push('bitbucket.token (or BITBUCKET_ACCESS_TOKEN)');
59
- console.error(`[atlassian-mcp] Bitbucket disabled: missing ${missing.join(', ')}`);
60
- }
61
- return config;
62
- }
package/dist/context.js DELETED
@@ -1,162 +0,0 @@
1
- import { execSync } from 'child_process';
2
- import { parseBitbucketRemote } from './bitbucket.js';
3
- const JIRA_KEY_RE = /\b[A-Z][A-Z0-9]+-\d+\b/g;
4
- function safeExec(cmd, cwd) {
5
- try {
6
- return execSync(cmd, { cwd, encoding: 'utf-8' }).trim();
7
- }
8
- catch {
9
- return '';
10
- }
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
- }
33
- /**
34
- * Unified developer context: git state + linked Jira issues + open PR for current branch.
35
- * Either jira or bitbucket may be null when only one product is configured.
36
- */
37
- export async function getDevContext(args, jira, bitbucket) {
38
- const repoPath = args.repoPath ?? process.cwd();
39
- const sections = [];
40
- const branch = safeExec('git rev-parse --abbrev-ref HEAD', repoPath) || '(unknown)';
41
- const remote = safeExec('git remote get-url origin', repoPath) || '(no remote)';
42
- const recentCommits = safeExec('git log --oneline -5', repoPath) || '(none)';
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;
46
- // Upstream ahead/behind
47
- const upstream = safeExec('git rev-parse --abbrev-ref @{u}', repoPath);
48
- let upstreamLine = '';
49
- if (upstream) {
50
- const ab = safeExec(`git rev-list --left-right --count ${upstream}...HEAD`, repoPath);
51
- if (ab.includes('\t')) {
52
- const [behind, ahead] = ab.split('\t').map(Number);
53
- const parts = [];
54
- if (ahead)
55
- parts.push(`${ahead} ahead`);
56
- if (behind)
57
- parts.push(`${behind} behind`);
58
- upstreamLine = `${upstream}${parts.length ? ` (${parts.join(', ')})` : ' (up to date)'}`;
59
- }
60
- }
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 = [
72
- `Repository: ${repoPath}`,
73
- `Branch: ${branch}`,
74
- ...(upstreamLine ? [`Upstream: ${upstreamLine}`] : []),
75
- `Remote: ${remote}`,
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'));
95
- // Jira — fetch overview for any tickets referenced in the branch name (parallel)
96
- const jiraKeys = jira ? [...new Set(branch.match(JIRA_KEY_RE) ?? [])] : [];
97
- const jiraResults = await Promise.all(jiraKeys.map(async (key) => {
98
- try {
99
- const result = await jira.issueOverview({
100
- issueKey: key,
101
- includeComments: true,
102
- commentsMaxResults: 5,
103
- includeTransitions: true,
104
- includeSprint: true,
105
- descriptionMaxChars: 800,
106
- });
107
- return `── Jira ${key} ──\n${result.content[0].text}`;
108
- }
109
- catch {
110
- return `── Jira ${key} ── (could not fetch)`;
111
- }
112
- }));
113
- sections.push(...jiraResults);
114
- // Bitbucket — find the open PR for this branch (only if remote points to this instance)
115
- if (parsed) {
116
- try {
117
- const pr = await bitbucket.findOpenPrForBranch(parsed.projectKey, parsed.repoSlug, branch);
118
- if (pr) {
119
- const approved = pr.reviewers.filter((r) => r.approved).length;
120
- const total = pr.reviewers.length;
121
- const reviewers = pr.reviewers.map((r) => `${r.user.displayName}${r.approved ? ' ✓' : ''}`).join(', ');
122
- const url = pr.links?.self?.[0]?.href ?? '';
123
- const descSnippet = pr.description
124
- ? pr.description.slice(0, 200) + (pr.description.length > 200 ? '…' : '')
125
- : '';
126
- const prLines = [
127
- `── PR #${pr.id}: ${pr.title} ──`,
128
- `State: ${pr.state}`,
129
- `Author: ${pr.author.user.displayName}`,
130
- `Branch: ${pr.fromRef.displayId} → ${pr.toRef.displayId}`,
131
- `Reviewers: ${reviewers || 'none'} (${approved}/${total} approved)`,
132
- ];
133
- if (url)
134
- prLines.push(`URL: ${url}`);
135
- if (descSnippet)
136
- prLines.push(``, descSnippet);
137
- // Workflow hint
138
- if (total > 0 && approved === total) {
139
- prLines.push(``, `✓ All reviewers approved — ready to merge: bitbucket_mutate {prId: ${pr.id}, action: "merge"}`);
140
- }
141
- else if (total > 0) {
142
- prLines.push(``, `→ ${total - approved} reviewer(s) pending — use bitbucket_get_pr {prId: ${pr.id}} to see open comments`);
143
- }
144
- else {
145
- prLines.push(``, `→ No reviewers assigned — use bitbucket_mutate {prId: ${pr.id}, update: {reviewers: [...]}} to add them`);
146
- }
147
- sections.push(prLines.join('\n'));
148
- }
149
- else {
150
- const noPrHint = [
151
- `── Bitbucket (${parsed.projectKey}/${parsed.repoSlug}) ── No open PR for branch "${branch}"`,
152
- `→ Create one: bitbucket_mutate {create: {title: "...", fromBranch: "${branch}"}}`,
153
- ];
154
- sections.push(noPrHint.join('\n'));
155
- }
156
- }
157
- catch {
158
- sections.push(`── Bitbucket (${parsed.projectKey}/${parsed.repoSlug}) ── (could not fetch PRs)`);
159
- }
160
- }
161
- return { content: [{ type: 'text', text: sections.join('\n\n') }] };
162
- }
package/dist/git.js DELETED
@@ -1,227 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- const JIRA_KEY_RE = /\b[A-Z][A-Z0-9]+-\d+\b/g;
3
- // Allowlist for git refs (commits, branches used as refs in diff commands)
4
- const SAFE_REF_RE = /^[a-zA-Z0-9/_.\-@{}~^:]+(\.\.\.[a-zA-Z0-9/_.\-@{}~^:]+)?$/;
5
- // Allowlist for branch names (stricter — no range syntax)
6
- const SAFE_BRANCH_RE = /^[a-zA-Z0-9/_.\-]+$/;
7
- function text(t) {
8
- return { content: [{ type: 'text', text: t }] };
9
- }
10
- function git(args, cwd) {
11
- return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
12
- }
13
- function safeGit(args, cwd, fallback = '') {
14
- try {
15
- return git(args, cwd);
16
- }
17
- catch {
18
- return fallback;
19
- }
20
- }
21
- function validateRepoPath(repoPath) {
22
- try {
23
- execFileSync('git', ['rev-parse', '--git-dir'], { cwd: repoPath, encoding: 'utf-8', stdio: 'pipe' });
24
- }
25
- catch {
26
- throw new Error(`Not a git repository: ${repoPath}`);
27
- }
28
- }
29
- function validateBranch(branch, label) {
30
- if (!SAFE_BRANCH_RE.test(branch)) {
31
- throw new Error(`Invalid ${label} "${branch}". Use only letters, numbers, /, _, ., -`);
32
- }
33
- }
34
- function validateRef(ref, label) {
35
- if (!SAFE_REF_RE.test(ref)) {
36
- throw new Error(`Invalid ${label} "${ref}". Use only safe git ref characters.`);
37
- }
38
- }
39
- export function getContext(args) {
40
- const repoPath = args.repoPath ?? process.cwd();
41
- const limit = Math.max(1, Math.min(args.commitLimit ?? 10, 100));
42
- try {
43
- validateRepoPath(repoPath);
44
- const branch = safeGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoPath, '(unknown)');
45
- const remote = safeGit(['remote', 'get-url', 'origin'], repoPath, '(no remote)');
46
- const commits = safeGit(['log', '--oneline', `-${limit}`], repoPath, '(no commits)');
47
- const status = safeGit(['status', '--short'], repoPath, '');
48
- // Upstream tracking
49
- const upstream = safeGit(['rev-parse', '--abbrev-ref', '@{u}'], repoPath, '');
50
- let upstreamLine = '';
51
- if (upstream) {
52
- const ab = safeGit(['rev-list', '--left-right', '--count', `${upstream}...HEAD`], repoPath, '');
53
- if (ab.includes('\t')) {
54
- const [behind, ahead] = ab.split('\t').map(Number);
55
- const parts = [];
56
- if (ahead)
57
- parts.push(`${ahead} ahead`);
58
- if (behind)
59
- parts.push(`${behind} behind`);
60
- upstreamLine = `${upstream}${parts.length ? ` (${parts.join(', ')})` : ' (up to date)'}`;
61
- }
62
- }
63
- // Diff stat summary
64
- const diffStatLines = safeGit(['diff', 'HEAD', '--stat'], repoPath, '').split('\n').filter(Boolean);
65
- const diffStat = diffStatLines[diffStatLines.length - 1]?.trim() ?? '';
66
- const jiraKeys = [...new Set(branch.match(JIRA_KEY_RE) ?? [])];
67
- const lines = [
68
- `Repository: ${repoPath}`,
69
- `Branch: ${branch}`,
70
- ...(upstreamLine ? [`Upstream: ${upstreamLine}`] : []),
71
- `Remote: ${remote}`,
72
- ...(jiraKeys.length ? [`Jira: ${jiraKeys.join(', ')}`] : []),
73
- '',
74
- `Recent commits (last ${limit}):`,
75
- commits || '(none)',
76
- '',
77
- 'Working tree:',
78
- ];
79
- if (status) {
80
- lines.push(status);
81
- if (diffStat)
82
- lines.push('', `Diff stat: ${diffStat}`);
83
- }
84
- else {
85
- lines.push('(clean)');
86
- }
87
- if (args.includeDiff && status) {
88
- const diff = safeGit(['diff', 'HEAD'], repoPath, '');
89
- if (diff) {
90
- const MAX = 6000;
91
- lines.push('', '── Uncommitted diff ──', diff.length > MAX ? diff.slice(0, MAX) + `\n\n... (truncated, ${diff.length - MAX} more chars)` : diff);
92
- }
93
- }
94
- return text(lines.join('\n'));
95
- }
96
- catch (err) {
97
- return text(`Error reading git context: ${err.message}`);
98
- }
99
- }
100
- // Not exposed as an MCP tool — internal/experimental use only
101
- export function getCommits(args) {
102
- const repoPath = args.repoPath ?? process.cwd();
103
- const limit = Math.max(1, Math.min(args.limit ?? 20, 200));
104
- const branch = args.branch ?? '';
105
- try {
106
- validateRepoPath(repoPath);
107
- if (branch)
108
- validateBranch(branch, 'branch');
109
- const format = '%H|%an|%ad|%s';
110
- const gitArgs = ['log', `--pretty=format:${format}`, '--date=short', `-${limit}`, ...(branch ? [branch] : [])];
111
- const raw = safeGit(gitArgs, repoPath, '');
112
- if (!raw)
113
- return text('No commits found.');
114
- const lines = raw.split('\n').map((line) => {
115
- const [hash, author, date, ...msgParts] = line.split('|');
116
- return `${hash?.slice(0, 8)} ${date} ${author}: ${msgParts.join('|')}`;
117
- });
118
- return text(`Last ${lines.length} commit(s)${branch ? ` on ${branch}` : ''}:\n${lines.join('\n')}`);
119
- }
120
- catch (err) {
121
- return text(`Error reading commits: ${err.message}`);
122
- }
123
- }
124
- export function getDiff(args) {
125
- const repoPath = args.repoPath ?? process.cwd();
126
- try {
127
- validateRepoPath(repoPath);
128
- let gitArgs;
129
- if (args.fromRef && args.toRef) {
130
- validateRef(args.fromRef, 'fromRef');
131
- validateRef(args.toRef, 'toRef');
132
- gitArgs = ['diff', args.fromRef, args.toRef];
133
- }
134
- else if (args.fromRef) {
135
- validateRef(args.fromRef, 'fromRef');
136
- gitArgs = ['diff', args.fromRef];
137
- }
138
- else {
139
- gitArgs = ['diff', 'HEAD'];
140
- }
141
- const diff = safeGit(gitArgs, repoPath, '');
142
- if (!diff)
143
- return text('No differences found.');
144
- return text(diff);
145
- }
146
- catch (err) {
147
- return text(`Error reading diff: ${err.message}`);
148
- }
149
- }
150
- export function checkRemoteBranch(branchName, repoPath) {
151
- validateBranch(branchName, 'branchName');
152
- const lsRemote = safeGit(['ls-remote', '--heads', 'origin', `refs/heads/${branchName}`], repoPath);
153
- if (!lsRemote)
154
- return { exists: false };
155
- const sha = lsRemote.split(/\s+/)[0]?.trim();
156
- // Fetch so we can read the log — failure is non-fatal (network/credentials issue)
157
- try {
158
- git(['fetch', 'origin', branchName], repoPath);
159
- }
160
- catch {
161
- return { exists: true, sha: sha?.slice(0, 8) };
162
- }
163
- const log = safeGit(['log', `origin/${branchName}`, '-1', '--format=%an%x09%ae%x09%ad%x09%s'], repoPath);
164
- if (!log)
165
- return { exists: true, sha: sha?.slice(0, 8) };
166
- const [author, email, date, ...msgParts] = log.split('\t');
167
- return {
168
- exists: true,
169
- sha: sha?.slice(0, 8),
170
- author: email ? `${author} <${email}>` : author,
171
- date,
172
- message: msgParts.join('\t'),
173
- };
174
- }
175
- function getDefaultBranch(repoPath) {
176
- const head = safeGit(['rev-parse', '--abbrev-ref', 'origin/HEAD'], repoPath);
177
- if (head && head.startsWith('origin/'))
178
- return head.slice('origin/'.length);
179
- // origin/HEAD not set — probe common defaults
180
- if (safeGit(['rev-parse', '--verify', 'origin/main'], repoPath))
181
- return 'main';
182
- return 'master';
183
- }
184
- export function checkoutRemoteBranch(branchName, repoPath) {
185
- try {
186
- validateBranch(branchName, 'branchName');
187
- const existing = safeGit(['branch', '--list', branchName], repoPath);
188
- if (existing.trim()) {
189
- git(['checkout', branchName], repoPath);
190
- return text(`Switched to existing local branch "${branchName}".`);
191
- }
192
- git(['checkout', '--track', `origin/${branchName}`], repoPath);
193
- return text(`Checked out "${branchName}" tracking origin/${branchName}.`);
194
- }
195
- catch (err) {
196
- return text(`Error checking out branch: ${err.message}`);
197
- }
198
- }
199
- export function createBranch(args) {
200
- const repoPath = args.repoPath ?? process.cwd();
201
- const { branchName, push = false } = args;
202
- try {
203
- validateRepoPath(repoPath);
204
- if (!SAFE_BRANCH_RE.test(branchName)) {
205
- return text(`Invalid branch name "${branchName}". Use only letters, numbers, /, _, ., -`);
206
- }
207
- const baseBranch = args.baseBranch ?? getDefaultBranch(repoPath);
208
- if (!SAFE_BRANCH_RE.test(baseBranch)) {
209
- return text(`Invalid base branch name "${baseBranch}". Use only letters, numbers, /, _, ., -`);
210
- }
211
- const existing = safeGit(['branch', '--list', branchName], repoPath);
212
- if (existing.trim()) {
213
- return text(`Branch "${branchName}" already exists locally. Switch with: git checkout ${branchName}`);
214
- }
215
- safeGit(['fetch', 'origin', baseBranch], repoPath);
216
- git(['checkout', '-b', branchName, `origin/${baseBranch}`], repoPath);
217
- const lines = [`Created and switched to branch "${branchName}" from origin/${baseBranch}.`];
218
- if (push) {
219
- git(['push', '-u', 'origin', branchName], repoPath);
220
- lines.push(`Pushed to origin/${branchName} and set upstream.`);
221
- }
222
- return text(lines.join('\n'));
223
- }
224
- catch (err) {
225
- return text(`Error creating branch: ${err.message}`);
226
- }
227
- }