@stubbedev/atlassian-mcp 0.5.24 → 0.5.26

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/README.md CHANGED
@@ -124,8 +124,12 @@ cannot get them wrong, and they need no prompting:
124
124
  (including worklog comments) and Bitbucket PR description / PR comment posted
125
125
  through this server ends with a bold `[AI]` line, added after markdown→wiki
126
126
  conversion so it never lands inside a code block, and skipped rather than
127
- mangled when the text ends inside an open code fence. Disable with
128
- `"markAIText": false` in the config file or `ATLASSIAN_MCP_MARK_AI_TEXT=false`.
127
+ mangled when the text ends inside an open code fence. A comment the user
128
+ explicitly asked the agent to post for them is left unmarked via
129
+ `commentUserAuthored=true` (`jira_mutate`) or `userAuthored=true`
130
+ (`bitbucket_comment`), so `[AI]` only flags text the agent wrote on its own.
131
+ Disable marking entirely with `"markAIText": false` in the config file or
132
+ `ATLASSIAN_MCP_MARK_AI_TEXT=false`.
129
133
 
130
134
  ## Setup
131
135
 
@@ -235,6 +239,15 @@ you declined the plugin: it downloads the binary on its first run, then hands ov
235
239
  Clients start the server from their own working directory, so use absolute paths. Set
236
240
  `ATLASSIAN_MCP_SKIP_DOWNLOAD=1` to skip the install-time download.
237
241
 
242
+ #### Binary cache
243
+
244
+ npm and Composer download each release's binary once per user, into
245
+ `$XDG_CACHE_HOME/atlassian-mcp/<version>/` (by default `~/.cache` on Linux and FreeBSD,
246
+ `~/Library/Caches` on macOS, `%LOCALAPPDATA%` on Windows), and hardlink it into every
247
+ install, or copy it where the install is on another filesystem. Further projects and git
248
+ worktrees on the same version install without downloading anything. Delete old version
249
+ directories to reclaim their space; installs keep their own link.
250
+
238
251
  #### Standalone binary (Go, Nix, release download)
239
252
 
240
253
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.5.24",
3
+ "version": "0.5.26",
4
4
  "description": "MCP server for self-hosted Jira and Bitbucket (Go, distributed as a prebuilt binary)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,23 +1,18 @@
1
- // Resolves (and, if needed, downloads) the prebuilt Go binary that matches the
2
- // current platform. Shared by the postinstall script and the CLI launcher so a
3
- // failed install (e.g. offline) self-heals on first run.
4
- import { createWriteStream } from 'node:fs';
5
- import { chmod, mkdir, rm, stat, rename } from 'node:fs/promises';
1
+ import { randomBytes } from 'node:crypto';
2
+ import { createWriteStream, readFileSync } from 'node:fs';
3
+ import { chmod, copyFile, link, mkdir, rename, rm, stat } from 'node:fs/promises';
4
+ import { dirname, join } from 'node:path';
6
5
  import { Readable } from 'node:stream';
7
6
  import { pipeline } from 'node:stream/promises';
8
- import { dirname, join } from 'node:path';
9
7
  import { fileURLToPath } from 'node:url';
10
- import { readFileSync } from 'node:fs';
11
8
 
12
- const here = dirname(fileURLToPath(import.meta.url));
13
- const root = join(here, '..');
14
- const binDir = join(root, 'bin');
9
+ const NAME = 'atlassian-mcp';
10
+ const REPO = `stubbedev/${NAME}`;
11
+ const SOURCE_HINT = `go install -tags nodynamic github.com/${REPO}@latest`;
15
12
 
13
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
16
14
  const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
17
- const REPO = 'stubbedev/atlassian-mcp';
18
15
 
19
- // Map Node's platform/arch onto the Go release asset naming. Anything not
20
- // listed falls back to the "go install" hint in target().
21
16
  const PLATFORMS = {
22
17
  'linux:x64': { os: 'linux', arch: 'amd64', ext: '' },
23
18
  'linux:arm64': { os: 'linux', arch: 'arm64', ext: '' },
@@ -35,52 +30,88 @@ const PLATFORMS = {
35
30
  'freebsd:arm64': { os: 'freebsd', arch: 'arm64', ext: '' },
36
31
  };
37
32
 
33
+ export async function ensureBinary() {
34
+ const dest = join(root, 'bin', `${NAME}-native${target().ext}`);
35
+ if (await present(dest)) return dest;
36
+ const cached = join(cacheDir(), asset());
37
+ if (!(await present(cached))) await fetchInto(cached);
38
+ await place(cached, dest);
39
+ return dest;
40
+ }
41
+
38
42
  function target() {
39
43
  const key = `${process.platform}:${process.arch}`;
40
44
  const t = PLATFORMS[key];
41
45
  if (!t) {
42
- throw new Error(
43
- `Unsupported platform ${key}. Build from source with: go install github.com/${REPO}@latest`,
44
- );
46
+ throw new Error(`Unsupported platform ${key}. Build from source with: ${SOURCE_HINT}`);
45
47
  }
46
48
  return t;
47
49
  }
48
50
 
49
- export function binaryPath() {
50
- const { ext } = target();
51
- return join(binDir, `atlassian-mcp-native${ext}`);
51
+ function asset() {
52
+ const { os, arch, ext } = target();
53
+ return `${NAME}_${os}_${arch}${ext}`;
52
54
  }
53
55
 
54
- function assetUrl() {
55
- const { os, arch, ext } = target();
56
- return `https://github.com/${REPO}/releases/download/v${pkg.version}/atlassian-mcp_${os}_${arch}${ext}`;
56
+ async function fetchInto(cached) {
57
+ const url = `https://github.com/${REPO}/releases/download/v${pkg.version}/${asset()}`;
58
+ const res = await fetch(url, { redirect: 'follow' });
59
+ if (!res.ok || !res.body) {
60
+ throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
61
+ }
62
+ const tmp = await tmpBeside(cached);
63
+ try {
64
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(tmp));
65
+ if ((await stat(tmp)).size === 0) {
66
+ throw new Error(`Failed to download ${url}: empty response`);
67
+ }
68
+ await commit(tmp, cached);
69
+ } finally {
70
+ await rm(tmp, { force: true });
71
+ }
72
+ }
73
+
74
+ function cacheDir() {
75
+ return join(cacheRoot(), NAME, pkg.version);
76
+ }
77
+
78
+ function cacheRoot() {
79
+ if (process.env.XDG_CACHE_HOME) return process.env.XDG_CACHE_HOME;
80
+ const [variable, ...suffix] =
81
+ { win32: ['LOCALAPPDATA'], darwin: ['HOME', 'Library', 'Caches'] }[process.platform] ?? ['HOME', '.cache'];
82
+ const base = process.env[variable];
83
+ if (!base) {
84
+ throw new Error(`Cannot locate a cache directory: neither XDG_CACHE_HOME nor ${variable} is set`);
85
+ }
86
+ return join(base, ...suffix);
57
87
  }
58
88
 
59
- async function exists(path) {
89
+ async function present(path) {
60
90
  try {
61
91
  const s = await stat(path);
62
92
  return s.isFile() && s.size > 0;
63
- } catch {
64
- return false;
93
+ } catch (err) {
94
+ if (err.code === 'ENOENT') return false;
95
+ throw err;
65
96
  }
66
97
  }
67
98
 
68
- // ensureBinary returns the path to the platform binary, downloading it from the
69
- // matching GitHub release if it is not already present.
70
- export async function ensureBinary() {
71
- const dest = binaryPath();
72
- if (await exists(dest)) return dest;
73
-
74
- await mkdir(binDir, { recursive: true });
75
- const url = assetUrl();
76
- const res = await fetch(url, { redirect: 'follow' });
77
- if (!res.ok || !res.body) {
78
- throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
99
+ async function place(cached, dest) {
100
+ const tmp = await tmpBeside(dest);
101
+ try {
102
+ await link(cached, tmp).catch(() => copyFile(cached, tmp));
103
+ await commit(tmp, dest);
104
+ } finally {
105
+ await rm(tmp, { force: true });
79
106
  }
80
- const tmp = `${dest}.download`;
81
- await pipeline(Readable.fromWeb(res.body), createWriteStream(tmp));
82
- await chmod(tmp, 0o755).catch(() => {});
83
- await rm(dest, { force: true });
107
+ }
108
+
109
+ async function commit(tmp, dest) {
110
+ await chmod(tmp, 0o755);
84
111
  await rename(tmp, dest);
85
- return dest;
112
+ }
113
+
114
+ async function tmpBeside(file) {
115
+ await mkdir(dirname(file), { recursive: true });
116
+ return `${file}.${randomBytes(6).toString('hex')}.tmp`;
86
117
  }