@stubbedev/atlassian-mcp 0.5.25 → 0.5.27
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 +10 -0
- package/package.json +1 -1
- package/scripts/download.mjs +87 -41
package/README.md
CHANGED
|
@@ -239,6 +239,16 @@ you declined the plugin: it downloads the binary on its first run, then hands ov
|
|
|
239
239
|
Clients start the server from their own working directory, so use absolute paths. Set
|
|
240
240
|
`ATLASSIAN_MCP_SKIP_DOWNLOAD=1` to skip the install-time download.
|
|
241
241
|
|
|
242
|
+
#### Binary cache
|
|
243
|
+
|
|
244
|
+
npm and Composer download each release's binary once per user, verify it against the
|
|
245
|
+
release's `checksums.txt`, and keep it in `$XDG_CACHE_HOME/atlassian-mcp/<version>/` (by
|
|
246
|
+
default `~/.cache` on Linux and FreeBSD, `~/Library/Caches` on macOS, `%LOCALAPPDATA%` on
|
|
247
|
+
Windows), and hardlink it into every install, or copy it where the install is on another
|
|
248
|
+
filesystem. Further projects and git worktrees on the same version install without
|
|
249
|
+
downloading anything. Delete old version directories to reclaim their space; installs keep
|
|
250
|
+
their own link.
|
|
251
|
+
|
|
242
252
|
#### Standalone binary (Go, Nix, release download)
|
|
243
253
|
|
|
244
254
|
```bash
|
package/package.json
CHANGED
package/scripts/download.mjs
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { createWriteStream } from 'node:fs';
|
|
5
|
-
import { chmod, mkdir, rm, stat, rename } from 'node:fs/promises';
|
|
6
|
-
import { Readable } from 'node:stream';
|
|
7
|
-
import { pipeline } from 'node:stream/promises';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { chmod, copyFile, link, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
8
4
|
import { dirname, join } from 'node:path';
|
|
9
5
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { readFileSync } from 'node:fs';
|
|
11
6
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
7
|
+
const NAME = 'atlassian-mcp';
|
|
8
|
+
const REPO = `stubbedev/${NAME}`;
|
|
9
|
+
const SOURCE_HINT = `go install -tags nodynamic github.com/${REPO}@latest`;
|
|
15
10
|
|
|
11
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
16
12
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
|
|
17
|
-
const REPO = 'stubbedev/atlassian-mcp';
|
|
18
13
|
|
|
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
14
|
const PLATFORMS = {
|
|
22
15
|
'linux:x64': { os: 'linux', arch: 'amd64', ext: '' },
|
|
23
16
|
'linux:arm64': { os: 'linux', arch: 'arm64', ext: '' },
|
|
@@ -35,52 +28,105 @@ const PLATFORMS = {
|
|
|
35
28
|
'freebsd:arm64': { os: 'freebsd', arch: 'arm64', ext: '' },
|
|
36
29
|
};
|
|
37
30
|
|
|
31
|
+
export async function ensureBinary() {
|
|
32
|
+
const dest = join(root, 'bin', `${NAME}-native${target().ext}`);
|
|
33
|
+
if (await present(dest)) return dest;
|
|
34
|
+
const cached = join(cacheDir(), asset());
|
|
35
|
+
if (!(await present(cached))) await fetchInto(cached);
|
|
36
|
+
await place(cached, dest);
|
|
37
|
+
return dest;
|
|
38
|
+
}
|
|
39
|
+
|
|
38
40
|
function target() {
|
|
39
41
|
const key = `${process.platform}:${process.arch}`;
|
|
40
42
|
const t = PLATFORMS[key];
|
|
41
43
|
if (!t) {
|
|
42
|
-
throw new Error(
|
|
43
|
-
`Unsupported platform ${key}. Build from source with: go install github.com/${REPO}@latest`,
|
|
44
|
-
);
|
|
44
|
+
throw new Error(`Unsupported platform ${key}. Build from source with: ${SOURCE_HINT}`);
|
|
45
45
|
}
|
|
46
46
|
return t;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
const { ext } = target();
|
|
51
|
-
return
|
|
49
|
+
function asset() {
|
|
50
|
+
const { os, arch, ext } = target();
|
|
51
|
+
return `${NAME}_${os}_${arch}${ext}`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
return `https://github.com/${REPO}/releases/download/v${pkg.version}/atlassian-mcp_${os}_${arch}${ext}`;
|
|
54
|
+
async function fetchInto(cached) {
|
|
55
|
+
await store(await fetchRelease(`v${pkg.version}`, asset()), cached);
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
function cacheDir() {
|
|
59
|
+
return join(cacheRoot(), NAME, pkg.version);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cacheRoot() {
|
|
63
|
+
if (process.env.XDG_CACHE_HOME) return process.env.XDG_CACHE_HOME;
|
|
64
|
+
const [variable, ...suffix] =
|
|
65
|
+
{ win32: ['LOCALAPPDATA'], darwin: ['HOME', 'Library', 'Caches'] }[process.platform] ?? ['HOME', '.cache'];
|
|
66
|
+
const base = process.env[variable];
|
|
67
|
+
if (!base) {
|
|
68
|
+
throw new Error(`Cannot locate a cache directory: neither XDG_CACHE_HOME nor ${variable} is set`);
|
|
69
|
+
}
|
|
70
|
+
return join(base, ...suffix);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function present(path) {
|
|
60
74
|
try {
|
|
61
75
|
const s = await stat(path);
|
|
62
76
|
return s.isFile() && s.size > 0;
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.code === 'ENOENT') return false;
|
|
79
|
+
throw err;
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
async function place(cached, dest) {
|
|
84
|
+
const tmp = await tmpBeside(dest);
|
|
85
|
+
try {
|
|
86
|
+
await link(cached, tmp).catch(() => copyFile(cached, tmp));
|
|
87
|
+
await commit(tmp, dest);
|
|
88
|
+
} finally {
|
|
89
|
+
await rm(tmp, { force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function commit(tmp, dest) {
|
|
94
|
+
await chmod(tmp, 0o755);
|
|
95
|
+
await rename(tmp, dest);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function tmpBeside(file) {
|
|
99
|
+
await mkdir(dirname(file), { recursive: true });
|
|
100
|
+
return `${file}.${randomBytes(6).toString('hex')}.tmp`;
|
|
101
|
+
}
|
|
73
102
|
|
|
74
|
-
|
|
75
|
-
const
|
|
103
|
+
async function fetchRelease(tag, asset) {
|
|
104
|
+
const base = `https://github.com/${REPO}/releases/download/${tag}`;
|
|
105
|
+
const sums = (await get(`${base}/checksums.txt`)).toString('utf8');
|
|
106
|
+
const want = sums
|
|
107
|
+
.split('\n')
|
|
108
|
+
.map((line) => line.trim().split(/\s+\*?/))
|
|
109
|
+
.find(([, file]) => file === asset)?.[0];
|
|
110
|
+
if (!want) throw new Error(`${asset} is not listed in ${tag} checksums.txt`);
|
|
111
|
+
const data = await get(`${base}/${asset}`);
|
|
112
|
+
if (createHash('sha256').update(data).digest('hex') !== want) {
|
|
113
|
+
throw new Error(`Checksum mismatch for ${asset} from ${tag}`);
|
|
114
|
+
}
|
|
115
|
+
return data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function get(url) {
|
|
76
119
|
const res = await fetch(url, { redirect: 'follow' });
|
|
77
|
-
if (!res.ok
|
|
78
|
-
|
|
120
|
+
if (!res.ok) throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
|
|
121
|
+
return Buffer.from(await res.arrayBuffer());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function store(data, cached) {
|
|
125
|
+
const tmp = await tmpBeside(cached);
|
|
126
|
+
try {
|
|
127
|
+
await writeFile(tmp, data);
|
|
128
|
+
await commit(tmp, cached);
|
|
129
|
+
} finally {
|
|
130
|
+
await rm(tmp, { force: true });
|
|
79
131
|
}
|
|
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 });
|
|
84
|
-
await rename(tmp, dest);
|
|
85
|
-
return dest;
|
|
86
132
|
}
|