@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/README.md CHANGED
@@ -38,10 +38,10 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **s
38
38
 
39
39
  | Tool | Description |
40
40
  |---|---|
41
- | `bitbucket_search` | Discover resources: `pull_requests` (default), `repos`, or `branches` via `resource` param; `mine=true` for your inbox |
41
+ | `bitbucket_search` | Discover resources: `pull_requests` (default), `repos`, `branches`, or `users` via `resource` param; `mine=true` for your inbox |
42
42
  | `bitbucket_get_pr` | Full PR details: metadata, commits, comments, blockers, build status, optional diff, and any attachments referenced from the description or comments |
43
43
  | `bitbucket_get_attachment` | Fetch a repo attachment by ID. Same decoding pipeline as `jira_get_attachment` (images, videos, animated images, audio, PDFs). Oversized or non-renderable attachments are auto-saved to a temp file and the path is returned; `saveTo` streams the original to disk |
44
- | `bitbucket_mutate` | Create/update a PR, or perform lifecycle actions: `approve`, `unapprove`, `merge`, `decline` |
44
+ | `bitbucket_mutate` | Create/update a PR, or perform lifecycle actions: `approve`, `unapprove`, `needs_work`, `merge`, `decline` |
45
45
  | `bitbucket_comment` | Add, update, or delete a PR comment; for code changes use `suggestion` so Bitbucket shows Apply suggestion (no trailing text after a suggestion block) |
46
46
  | `bitbucket_get_file` | Raw file content from Bitbucket at a branch, tag, or commit |
47
47
  | `bitbucket_pr_tasks` | Manage PR tasks (checklist items): `list`, `create`, `resolve`, `reopen`, `delete` |
@@ -114,7 +114,7 @@ BITBUCKET_URL=https://bitbucket.example.com
114
114
  BITBUCKET_ACCESS_TOKEN=your-bitbucket-personal-access-token
115
115
  ```
116
116
 
117
- Config is resolved in this order: `--config <path>` CLI arg → `ATLASSIAN_MCP_CONFIG` env var → `~/.atlassian-mcp.json` → `.atlassian-mcp.json` in cwd → environment variables.
117
+ Config is resolved in this order: `--config <path>` CLI arg → `ATLASSIAN_MCP_CONFIG` env var → `~/.atlassian-mcp.json` → `$XDG_CONFIG_HOME/atlassian-mcp/config.json` (default `~/.config/atlassian-mcp/config.json`) → `.atlassian-mcp.json` in cwd → environment variables.
118
118
 
119
119
  ### 2. Connect to your AI tool
120
120
 
@@ -236,17 +236,23 @@ Then restart your MCP client.
236
236
 
237
237
  ---
238
238
 
239
- ### Manual install (optional)
239
+ ### Install without npm
240
240
 
241
- If you prefer to clone and run locally:
241
+ The server is a single static Go binary. The `npx` path above downloads the prebuilt
242
+ binary for your platform on first run; these alternatives skip Node entirely:
242
243
 
243
244
  ```bash
244
- git clone git@github.com:stubbedev/atlassian-mcp.git
245
- cd atlassian-mcp
246
- npm install
245
+ # Go toolchain — installs to $GOBIN / $GOPATH/bin
246
+ go install github.com/stubbedev/atlassian-mcp@latest
247
+
248
+ # Nix flake
249
+ nix run github:stubbedev/atlassian-mcp -- --config ~/.atlassian-mcp.json
247
250
  ```
248
251
 
249
- Then use `node /path/to/atlassian-mcp/dist/index.js` instead of the `npx` command in the configs above.
252
+ Then point your MCP client's `command` at the resulting `atlassian-mcp` binary
253
+ instead of `npx`. On these paths `ffmpeg`/`ffprobe` must be available on `PATH`
254
+ (or set `ATLASSIAN_MCP_FFMPEG_PATH` / `ATLASSIAN_MCP_FFPROBE_PATH`); the npm
255
+ wrapper bundles them automatically.
250
256
 
251
257
  ### Attachment decoding pipeline
252
258
 
@@ -254,28 +260,35 @@ The attachment tools (`jira_get_attachment`, `bitbucket_get_attachment`) decode
254
260
 
255
261
  | Input | What gets returned | How |
256
262
  | --- | --- | --- |
257
- | Static images (PNG/JPEG/WebP/AVIF/SVG…) | Resized image content blocks | `sharp` (long edge ≤ `maxDimension`, default 1568) |
258
- | Animated images (GIF/APNG/animated WebP) | N sampled frames as image content blocks | `ffmpeg-static` + `sharp` (default 6 frames @ 768 px) |
259
- | Video (mp4/webm/mov/…) | N sampled frames as image content blocks | `ffmpeg-static` + `sharp`. Uniform or scene-change sampling. Re-call with `start`, `end`, `frames`, `mode`, `sceneThreshold` to zoom in |
263
+ | Static images (PNG/JPEG/WebP/BMP/TIFF/GIF/SVG…) | Resized image content blocks | native Go (`imaging`, long edge ≤ `maxDimension`, default 1568; EXIF auto-orient; PNG for alpha, else JPEG) |
264
+ | Animated images (GIF/APNG/animated WebP) | N sampled frames as image content blocks | `ffmpeg` + native Go re-encode (default 6 frames @ 768 px) |
265
+ | Video (mp4/webm/mov/…) | N sampled frames as image content blocks | `ffmpeg`/`ffprobe`. Uniform or scene-change sampling. Re-call with `start`, `end`, `frames`, `mode`, `sceneThreshold` to zoom in |
260
266
  | Audio (mp3/wav/ogg/…) | MCP audio content block | passthrough |
261
- | PDFs | Extracted text — or rasterized pages if text is empty (scanned PDFs) | `unpdf` + `@napi-rs/canvas` |
267
+ | PDFs | Extracted text — or rasterized pages if text is empty (scanned PDFs) | native Go text extraction (`ledongthuc/pdf`); rasterization shells to `pdftoppm`/`mutool` if present, else the original is saved to disk |
262
268
  | Text-like (json/xml/yaml/…) | Text content block | passthrough |
263
- | Everything else (or oversized) | Auto-saved to a temp file; path is returned | `os.tmpdir()` with `atlmcp-` prefix |
269
+ | Everything else (or oversized) | Auto-saved to a temp file; path is returned | `os.TempDir()` with `atlmcp-` prefix |
264
270
 
265
271
  Auto-saved files are periodically pruned by TTL and total-size quota — see *Environment overrides* below.
266
272
 
267
- ### Native dependencies
273
+ ### External tools (optional)
274
+
275
+ Image and PDF-text decoding are pure Go and need nothing extra. The two pipelines that have no
276
+ pure-Go implementation shell out to external binaries:
268
277
 
269
- - [`sharp`](https://sharp.pixelplumbing.com/)image decode/resize. Ships prebuilt binaries for glibc Linux (x64/arm64), macOS, Windows. Alpine / musl users may need `npm install --cpu=x64 --os=linux --libc=musl sharp`.
270
- - [`ffmpeg-static`](https://www.npmjs.com/package/ffmpeg-static) + [`ffprobe-static`](https://www.npmjs.com/package/ffprobe-static) — video/audio decode. ~80 MB bundled binary per platform. Override with env vars (below) if you have system ffmpeg.
271
- - [`@napi-rs/canvas`](https://www.npmjs.com/package/@napi-rs/canvas) + [`unpdf`](https://www.npmjs.com/package/unpdf) PDF text extraction and page rasterization.
278
+ - **`ffmpeg` + `ffprobe`**video and animated-image frame sampling. The npm wrapper bundles
279
+ [`ffmpeg-static`](https://www.npmjs.com/package/ffmpeg-static) /
280
+ [`ffprobe-static`](https://www.npmjs.com/package/ffprobe-static) and injects their paths, so the
281
+ npx install path is zero-config. On the `go install` / Nix paths, install `ffmpeg` (it provides
282
+ `ffprobe`) or set the env vars below.
283
+ - **`pdftoppm` (poppler) or `mutool` (MuPDF)** — only needed to rasterize *scanned* PDFs that have no
284
+ extractable text. If neither is on `PATH`, such PDFs are saved to disk instead.
272
285
 
273
286
  ### Environment overrides
274
287
 
275
288
  | Variable | Purpose | Default |
276
289
  | --- | --- | --- |
277
- | `ATLASSIAN_MCP_FFMPEG_PATH` | Path to `ffmpeg` binary. Overrides `ffmpeg-static`. Use this if you have system ffmpeg or `ffmpeg-static` doesn't ship for your platform (Alpine/musl, some ARM variants). | bundled `ffmpeg-static` |
278
- | `ATLASSIAN_MCP_FFPROBE_PATH` | Path to `ffprobe` binary. Overrides `ffprobe-static`. | bundled `ffprobe-static` |
290
+ | `ATLASSIAN_MCP_FFMPEG_PATH` | Path to `ffmpeg` binary. | npm: bundled `ffmpeg-static`; otherwise `ffmpeg` on `PATH` |
291
+ | `ATLASSIAN_MCP_FFPROBE_PATH` | Path to `ffprobe` binary. | npm: bundled `ffprobe-static`; otherwise `ffprobe` on `PATH` |
279
292
  | `ATLASSIAN_MCP_TMP_TTL_DAYS` | Auto-saved attachments older than this are pruned. | `7` |
280
293
  | `ATLASSIAN_MCP_TMP_MAX_BYTES` | Total-size quota for auto-saved attachments in `os.tmpdir()`. When exceeded, oldest are evicted. | `1073741824` (1 GB) |
281
294
 
@@ -287,23 +300,20 @@ This package is published to npm as `@stubbedev/atlassian-mcp`.
287
300
 
288
301
  Use semantic versioning for releases. Breaking tool-surface changes should bump the minor version while `<1.0.0` (for example `0.0.x` -> `0.1.0`).
289
302
 
290
- Automatic publish is configured in `.github/workflows/publish.yml` and runs when a new version tag is pushed.
303
+ On a pushed `v*` tag, `.github/workflows/publish.yml` cross-compiles the Go binary for 14
304
+ OS/arch targets, attaches them to a GitHub release, and publishes the npm wrapper (which
305
+ downloads the matching binary on install).
291
306
 
292
307
  Release flow:
293
308
 
294
309
  ```bash
295
- # choose one: patch | minor | major
296
- increment=patch
297
-
298
- # bumps package.json + package-lock.json,
299
- # creates a version commit, and creates a git tag (for example v0.1.17)
300
- npm version "$increment"
301
-
302
- # push commit and tag to GitHub
310
+ # choose one: patch | minor | major (also: npm run release:patch / :minor / :major)
311
+ npm version patch # bumps package.json, commits, tags vX.Y.Z
303
312
  git push origin HEAD --follow-tags
304
313
  ```
305
314
 
306
- GitHub Actions will publish the npm release from that pushed tag.
315
+ `flake.nix` reads its version from `package.json`, so the Nix package tracks the same bump
316
+ automatically. GitHub Actions builds + publishes from the pushed tag.
307
317
 
308
318
  - The workflow is configured for npm Trusted Publisher (OIDC), so no `NPM_TOKEN` secret is required
309
319
 
@@ -351,22 +361,22 @@ Paste the token as the `token` value under `bitbucket` in your config file.
351
361
 
352
362
  ## Development
353
363
 
364
+ The server is a single Go module at the repo root (no `src/` tree).
365
+
354
366
  ```bash
355
- # Watch mode — recompiles on file changes
356
- npm run dev
367
+ # Build the binary
368
+ go build -o atlassian-mcp .
357
369
 
358
- # Run the built server directly
359
- node dist/index.js
370
+ # Run it
371
+ ./atlassian-mcp --config /path/to/config.json
372
+
373
+ # Vet + unit tests
374
+ go vet ./...
375
+ go test ./...
360
376
 
361
377
  # Test the tool list
362
- echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node dist/index.js
378
+ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | ./atlassian-mcp
363
379
 
364
- # Quick release smoke check
380
+ # Quick release smoke check (build + tools/list validation)
365
381
  npm run smoke
366
382
  ```
367
-
368
- To use a specific config file:
369
-
370
- ```bash
371
- node dist/index.js --config /path/to/config.json
372
- ```
package/bin/cli.mjs ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // npm/npx launcher. Ensures the prebuilt Go binary for this platform is present
3
+ // (npx @latest guarantees the newest version), then hands stdio to it directly.
4
+ //
5
+ // It also injects the bundled ffmpeg-static / ffprobe-static binaries via env so
6
+ // video and animated-image attachments work with zero extra setup on the npm
7
+ // install path. Users who install the Go binary directly (go install / Nix)
8
+ // supply ffmpeg/ffprobe on PATH, or set ATLASSIAN_MCP_FFMPEG_PATH / _FFPROBE_PATH.
9
+ //
10
+ // Note on overhead: with stdio 'inherit' the Go binary reads/writes the real
11
+ // stdin/stdout, so this launcher adds ZERO per-message latency — it only relays
12
+ // signals and the exit code.
13
+ import { spawn } from 'node:child_process';
14
+ import { ensureBinary } from '../scripts/download.mjs';
15
+
16
+ let bin;
17
+ try {
18
+ bin = await ensureBinary();
19
+ } catch (err) {
20
+ console.error(`[atlassian-mcp] ${err.message}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ // Resolve bundled ffmpeg/ffprobe and expose them to the Go binary unless the
25
+ // user already pinned a path. Best-effort: missing modules are non-fatal.
26
+ const env = { ...process.env };
27
+ async function resolveBundled(envKey, importer) {
28
+ if (env[envKey]) return;
29
+ try {
30
+ const p = await importer();
31
+ if (p) env[envKey] = p;
32
+ } catch { /* dependency absent — fall back to PATH lookup in the binary */ }
33
+ }
34
+ await resolveBundled('ATLASSIAN_MCP_FFMPEG_PATH', async () => {
35
+ const m = await import('ffmpeg-static');
36
+ return m.default ?? m;
37
+ });
38
+ await resolveBundled('ATLASSIAN_MCP_FFPROBE_PATH', async () => {
39
+ const m = await import('ffprobe-static');
40
+ return (m.default ?? m)?.path;
41
+ });
42
+
43
+ const child = spawn(bin, process.argv.slice(2), { stdio: 'inherit', env });
44
+
45
+ // Forward termination signals so the Go binary shuts down cleanly with its host.
46
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT']) {
47
+ process.on(sig, () => {
48
+ if (!child.killed) child.kill(sig);
49
+ });
50
+ }
51
+
52
+ child.on('exit', (code, signal) => {
53
+ if (signal) process.kill(process.pid, signal);
54
+ else process.exit(code ?? 0);
55
+ });
56
+ child.on('error', (err) => {
57
+ console.error(`[atlassian-mcp] failed to start binary: ${err.message}`);
58
+ process.exit(1);
59
+ });
package/package.json CHANGED
@@ -1,45 +1,32 @@
1
1
  {
2
2
  "name": "@stubbedev/atlassian-mcp",
3
- "version": "0.4.5",
4
- "description": "MCP server for self-hosted Jira and Bitbucket",
3
+ "version": "0.5.0",
4
+ "description": "MCP server for self-hosted Jira and Bitbucket (Go, distributed as a prebuilt binary)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "main": "dist/index.js",
7
+ "bin": {
8
+ "atlassian-mcp": "bin/cli.mjs"
9
+ },
8
10
  "files": [
9
- "dist",
11
+ "bin/cli.mjs",
12
+ "scripts/download.mjs",
13
+ "scripts/postinstall.mjs",
10
14
  "atlassian-mcp.schema.json",
11
15
  "README.md"
12
16
  ],
13
- "bin": {
14
- "atlassian-mcp": "dist/index.js"
15
- },
16
17
  "scripts": {
17
- "build": "tsc",
18
- "start": "node dist/index.js",
19
- "dev": "tsx src/index.ts",
20
- "release:patch": "npm version patch && git push origin && git push origin --tags && gh release create $(git describe --tags) --generate-notes",
21
- "release:minor": "npm version minor && git push origin && git push origin --tags && gh release create $(git describe --tags) --generate-notes",
22
- "release:major": "npm version major && git push origin && git push origin --tags && gh release create $(git describe --tags) --generate-notes",
23
- "smoke": "npm run build && npm run smoke:tools",
24
- "smoke:tools": "printf '%s\\n' '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}' | node dist/index.js",
25
- "smoke:validate": "printf '%s\\n' '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}' | node dist/index.js | node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const r=JSON.parse(d.trim());if(r.error)throw new Error('server error: '+r.error.message);const t=r.result?.tools;if(!Array.isArray(t)||!t.length)throw new Error('no tools in response');const bad=t.filter(x=>!x.name||!x.inputSchema);if(bad.length)throw new Error('malformed tools: '+bad.map(x=>x.name??'(unnamed)').join(', '));console.log('smoke OK — '+t.length+' tool(s): '+t.map(x=>x.name).join(', '))}catch(e){console.error('smoke FAIL:',e.message);process.exit(1)}})\"",
26
- "prerelease": "npm run build && npm run smoke:validate && ncu"
18
+ "postinstall": "node scripts/postinstall.mjs",
19
+ "build": "go build -o atlassian-mcp .",
20
+ "test": "go test ./...",
21
+ "smoke": "go build -o atlassian-mcp . && printf '%s\\n' '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}' | ATLASSIAN_MCP_CONFIG=/nonexistent JIRA_URL=https://github.com JIRA_ACCESS_TOKEN=smoke BITBUCKET_URL=https://github.com BITBUCKET_ACCESS_TOKEN=smoke ./atlassian-mcp | node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const r=JSON.parse(d.trim());if(r.error)throw new Error('server error: '+r.error.message);const t=r.result?.tools;if(!Array.isArray(t)||!t.length)throw new Error('no tools in response');const bad=t.filter(x=>!x.name||!x.inputSchema);if(bad.length)throw new Error('malformed tools: '+bad.map(x=>x.name??'(unnamed)').join(', '));console.log('smoke OK — '+t.length+' tool(s): '+t.map(x=>x.name).join(', '))}catch(e){console.error('smoke FAIL:',e.message);process.exit(1)}})\"",
22
+ "preversion": "go vet ./... && npm run test && npm run smoke",
23
+ "release:patch": "npm version patch && git push origin HEAD --follow-tags",
24
+ "release:minor": "npm version minor && git push origin HEAD --follow-tags",
25
+ "release:major": "npm version major && git push origin HEAD --follow-tags"
27
26
  },
28
27
  "dependencies": {
29
- "@modelcontextprotocol/sdk": "^1.29.0",
30
- "@napi-rs/canvas": "^0.1.100",
31
- "dotenv": "^17.4.2",
32
28
  "ffmpeg-static": "^5.3.0",
33
- "ffprobe-static": "^3.1.0",
34
- "pdfjs-dist": "^5.7.284",
35
- "sharp": "^0.35.2",
36
- "unpdf": "^1.6.2"
37
- },
38
- "devDependencies": {
39
- "@types/node": "^25.9.4",
40
- "npm-check-updates": "^21.0.3",
41
- "tsx": "^4.22.4",
42
- "typescript": "^6.0.3"
29
+ "ffprobe-static": "^3.1.0"
43
30
  },
44
31
  "engines": {
45
32
  "node": ">=18.0.0"
@@ -0,0 +1,86 @@
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';
6
+ import { Readable } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { readFileSync } from 'node:fs';
11
+
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ const root = join(here, '..');
14
+ const binDir = join(root, 'bin');
15
+
16
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
17
+ const REPO = 'stubbedev/atlassian-mcp';
18
+
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
+ const PLATFORMS = {
22
+ 'linux:x64': { os: 'linux', arch: 'amd64', ext: '' },
23
+ 'linux:arm64': { os: 'linux', arch: 'arm64', ext: '' },
24
+ 'linux:arm': { os: 'linux', arch: 'arm', ext: '' },
25
+ 'linux:ia32': { os: 'linux', arch: '386', ext: '' },
26
+ 'linux:ppc64': { os: 'linux', arch: 'ppc64le', ext: '' },
27
+ 'linux:s390x': { os: 'linux', arch: 's390x', ext: '' },
28
+ 'linux:riscv64': { os: 'linux', arch: 'riscv64', ext: '' },
29
+ 'darwin:x64': { os: 'darwin', arch: 'amd64', ext: '' },
30
+ 'darwin:arm64': { os: 'darwin', arch: 'arm64', ext: '' },
31
+ 'win32:x64': { os: 'windows', arch: 'amd64', ext: '.exe' },
32
+ 'win32:arm64': { os: 'windows', arch: 'arm64', ext: '.exe' },
33
+ 'win32:ia32': { os: 'windows', arch: '386', ext: '.exe' },
34
+ 'freebsd:x64': { os: 'freebsd', arch: 'amd64', ext: '' },
35
+ 'freebsd:arm64': { os: 'freebsd', arch: 'arm64', ext: '' },
36
+ };
37
+
38
+ function target() {
39
+ const key = `${process.platform}:${process.arch}`;
40
+ const t = PLATFORMS[key];
41
+ if (!t) {
42
+ throw new Error(
43
+ `Unsupported platform ${key}. Build from source with: go install github.com/${REPO}@latest`,
44
+ );
45
+ }
46
+ return t;
47
+ }
48
+
49
+ export function binaryPath() {
50
+ const { ext } = target();
51
+ return join(binDir, `atlassian-mcp-native${ext}`);
52
+ }
53
+
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}`;
57
+ }
58
+
59
+ async function exists(path) {
60
+ try {
61
+ const s = await stat(path);
62
+ return s.isFile() && s.size > 0;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
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}`);
79
+ }
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
+ }
@@ -0,0 +1,15 @@
1
+ // Best-effort download of the prebuilt binary at install time. Failures are not
2
+ // fatal — the CLI launcher retries the download on first run — so installs in
3
+ // offline/CI environments still succeed.
4
+ import { ensureBinary } from './download.mjs';
5
+
6
+ if (process.env.ATLASSIAN_MCP_SKIP_DOWNLOAD === '1') {
7
+ process.exit(0);
8
+ }
9
+
10
+ try {
11
+ await ensureBinary();
12
+ } catch (err) {
13
+ console.error(`[atlassian-mcp] postinstall: ${err.message}`);
14
+ console.error('[atlassian-mcp] The binary will be fetched on first run instead.');
15
+ }