@stubbedev/atlassian-mcp 0.0.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/config.js ADDED
@@ -0,0 +1,54 @@
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 missing = [];
38
+ if (!jiraUrl)
39
+ missing.push('jira.url (or JIRA_URL)');
40
+ if (!jiraToken)
41
+ missing.push('jira.token (or JIRA_ACCESS_TOKEN)');
42
+ if (!bitbucketUrl)
43
+ missing.push('bitbucket.url (or BITBUCKET_URL)');
44
+ if (!bitbucketToken)
45
+ missing.push('bitbucket.token (or BITBUCKET_ACCESS_TOKEN)');
46
+ if (missing.length > 0) {
47
+ throw new Error(`Missing required configuration: ${missing.join(', ')}.\n` +
48
+ 'Provide a config file (~/.atlassian-mcp.json or --config <path>) or set environment variables.');
49
+ }
50
+ return {
51
+ jira: { url: jiraUrl, token: jiraToken },
52
+ bitbucket: { url: bitbucketUrl, token: bitbucketToken },
53
+ };
54
+ }
@@ -0,0 +1,98 @@
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
+ /**
13
+ * Unified developer context: git state + linked Jira issues + open PR for current branch.
14
+ */
15
+ export async function getDevContext(args, jira, bitbucket) {
16
+ const repoPath = args.repoPath ?? process.cwd();
17
+ const sections = [];
18
+ const branch = safeExec('git rev-parse --abbrev-ref HEAD', repoPath) || '(unknown)';
19
+ const remote = safeExec('git remote get-url origin', repoPath) || '(no remote)';
20
+ const recentCommits = safeExec('git log --oneline -5', repoPath) || '(none)';
21
+ const status = safeExec('git status --short', repoPath) || '(clean)';
22
+ sections.push([
23
+ `Repository: ${repoPath}`,
24
+ `Branch: ${branch}`,
25
+ `Remote: ${remote}`,
26
+ '',
27
+ 'Recent commits:',
28
+ recentCommits,
29
+ '',
30
+ 'Working tree:',
31
+ status,
32
+ ].join('\n'));
33
+ // Jira — fetch any tickets referenced in the branch name
34
+ const jiraKeys = [...new Set(branch.match(JIRA_KEY_RE) ?? [])];
35
+ for (const key of jiraKeys) {
36
+ try {
37
+ const result = await jira.getIssue({ issueKey: key });
38
+ sections.push(`── Jira ${key} ──\n${result.content[0].text}`);
39
+ }
40
+ catch {
41
+ sections.push(`── Jira ${key} ── (could not fetch)`);
42
+ }
43
+ }
44
+ // Bitbucket — find the open PR for this branch
45
+ const parsed = parseBitbucketRemote(remote);
46
+ if (parsed) {
47
+ try {
48
+ const pr = await bitbucket.findOpenPrForBranch(parsed.projectKey, parsed.repoSlug, branch);
49
+ if (pr) {
50
+ const reviewers = pr.reviewers.map((r) => `${r.user.displayName}${r.approved ? ' ✓' : ''}`).join(', ');
51
+ const url = pr.links?.self?.[0]?.href ?? '';
52
+ const prLines = [
53
+ `── PR #${pr.id}: ${pr.title} ──`,
54
+ `State: ${pr.state}`,
55
+ `Author: ${pr.author.user.displayName}`,
56
+ `Branch: ${pr.fromRef.displayId} → ${pr.toRef.displayId}`,
57
+ `Reviewers: ${reviewers || 'None'}`,
58
+ ];
59
+ if (url)
60
+ prLines.push(`URL: ${url}`);
61
+ sections.push(prLines.join('\n'));
62
+ }
63
+ else {
64
+ sections.push(`── Bitbucket (${parsed.projectKey}/${parsed.repoSlug}) ── No open PR for branch "${branch}"`);
65
+ }
66
+ }
67
+ catch {
68
+ sections.push(`── Bitbucket (${parsed.projectKey}/${parsed.repoSlug}) ── (could not fetch PRs)`);
69
+ }
70
+ }
71
+ return { content: [{ type: 'text', text: sections.join('\n\n') }] };
72
+ }
73
+ /**
74
+ * Creates a Bitbucket PR using the current git repo to auto-detect project, repo, and branch.
75
+ */
76
+ export async function createPrFromContext(args, bitbucket) {
77
+ const repoPath = args.repoPath ?? process.cwd();
78
+ const remote = safeExec('git remote get-url origin', repoPath);
79
+ if (!remote)
80
+ throw new Error('No git remote found — are you in a git repository?');
81
+ const parsed = parseBitbucketRemote(remote);
82
+ if (!parsed)
83
+ throw new Error(`Could not parse Bitbucket project/repo from remote: ${remote}`);
84
+ const branch = safeExec('git rev-parse --abbrev-ref HEAD', repoPath);
85
+ if (!branch)
86
+ throw new Error('Could not determine current branch.');
87
+ if (branch === 'HEAD')
88
+ throw new Error('Detached HEAD state — check out a branch first.');
89
+ return bitbucket.createPullRequest({
90
+ projectKey: parsed.projectKey,
91
+ repoSlug: parsed.repoSlug,
92
+ title: args.title,
93
+ description: args.description,
94
+ fromBranch: branch,
95
+ toBranch: args.toBranch,
96
+ reviewers: args.reviewers,
97
+ });
98
+ }
package/dist/git.js ADDED
@@ -0,0 +1,86 @@
1
+ import { execSync } from 'child_process';
2
+ const JIRA_KEY_RE = /\b[A-Z][A-Z0-9]+-\d+\b/g;
3
+ function text(t) {
4
+ return { content: [{ type: 'text', text: t }] };
5
+ }
6
+ function git(cmd, cwd) {
7
+ return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8' }).trim();
8
+ }
9
+ function safeGit(cmd, cwd, fallback = '') {
10
+ try {
11
+ return git(cmd, cwd);
12
+ }
13
+ catch {
14
+ return fallback;
15
+ }
16
+ }
17
+ export function getContext(args) {
18
+ const repoPath = args.repoPath ?? process.cwd();
19
+ const limit = args.commitLimit ?? 10;
20
+ try {
21
+ const branch = safeGit('rev-parse --abbrev-ref HEAD', repoPath, '(unknown)');
22
+ const remote = safeGit('remote get-url origin', repoPath, '(no remote)');
23
+ const commits = safeGit(`log --oneline -${limit}`, repoPath, '(no commits)');
24
+ const status = safeGit('status --short', repoPath, '');
25
+ const jiraKeys = [...new Set(branch.match(JIRA_KEY_RE) ?? [])];
26
+ const lines = [
27
+ `Repository: ${repoPath}`,
28
+ `Branch: ${branch}`,
29
+ `Remote: ${remote}`,
30
+ ];
31
+ if (jiraKeys.length > 0) {
32
+ lines.push(`Jira issue(s) detected in branch: ${jiraKeys.join(', ')}`);
33
+ }
34
+ lines.push('', `Recent commits (last ${limit}):`, commits || '(none)', '', 'Working tree status:', status || '(clean)');
35
+ return text(lines.join('\n'));
36
+ }
37
+ catch (err) {
38
+ return text(`Error reading git context: ${err.message}`);
39
+ }
40
+ }
41
+ export function getCommits(args) {
42
+ const repoPath = args.repoPath ?? process.cwd();
43
+ const limit = args.limit ?? 20;
44
+ const branch = args.branch ?? '';
45
+ try {
46
+ const format = '%H|%an|%ad|%s';
47
+ const cmd = `log --pretty=format:"${format}" --date=short -${limit}${branch ? ` ${branch}` : ''}`;
48
+ const raw = safeGit(cmd, repoPath, '');
49
+ if (!raw)
50
+ return text('No commits found.');
51
+ const lines = raw.split('\n').map((line) => {
52
+ const [hash, author, date, ...msgParts] = line.replace(/^"|"$/g, '').split('|');
53
+ return `${hash?.slice(0, 8)} ${date} ${author}: ${msgParts.join('|')}`;
54
+ });
55
+ return text(`Last ${lines.length} commit(s)${branch ? ` on ${branch}` : ''}:\n${lines.join('\n')}`);
56
+ }
57
+ catch (err) {
58
+ return text(`Error reading commits: ${err.message}`);
59
+ }
60
+ }
61
+ export function getDiff(args) {
62
+ const repoPath = args.repoPath ?? process.cwd();
63
+ const MAX_CHARS = 8000;
64
+ try {
65
+ let cmd;
66
+ if (args.fromRef && args.toRef) {
67
+ cmd = `diff ${args.fromRef} ${args.toRef}`;
68
+ }
69
+ else if (args.fromRef) {
70
+ cmd = `diff ${args.fromRef}`;
71
+ }
72
+ else {
73
+ cmd = 'diff HEAD';
74
+ }
75
+ const diff = safeGit(cmd, repoPath, '');
76
+ if (!diff)
77
+ return text('No differences found.');
78
+ if (diff.length > MAX_CHARS) {
79
+ return text(diff.slice(0, MAX_CHARS) + `\n\n... (truncated, ${diff.length - MAX_CHARS} more chars)`);
80
+ }
81
+ return text(diff);
82
+ }
83
+ catch (err) {
84
+ return text(`Error reading diff: ${err.message}`);
85
+ }
86
+ }