@stubbedev/atlassian-mcp 0.4.5 → 0.5.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/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,65 @@ 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
250
+ ```
251
+
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.
256
+
257
+ ### Running as an HTTP server (shared / behind a proxy)
258
+
259
+ By default the server speaks MCP over **stdio** (one process per client, launched
260
+ by your editor). It can instead run as a long-lived **Streamable HTTP** server that
261
+ many clients share — useful behind a reverse proxy:
262
+
263
+ ```bash
264
+ atlassian-mcp --http # binds 127.0.0.1:7337
265
+ atlassian-mcp --http 127.0.0.1:9000 # custom address
266
+ ATLASSIAN_MCP_HTTP=1 atlassian-mcp # same, via env
247
267
  ```
248
268
 
249
- Then use `node /path/to/atlassian-mcp/dist/index.js` instead of the `npx` command in the configs above.
269
+ - Single endpoint `POST /mcp` (JSON-RPC) plus an optional `GET /mcp` SSE stream that
270
+ carries server→client requests (`roots/list`, elicitation). The server is **stateful**:
271
+ `initialize` mints a session and returns an `Mcp-Session-Id` header, which the client
272
+ **must** echo on every subsequent request and on the SSE stream. Requests with a
273
+ missing/unknown/expired session id get **HTTP 404** so the client re-initializes
274
+ (standard MCP-client behaviour). Each connected client/worktree is an isolated session.
275
+ - **Auth:** on a loopback bind no token is needed. Binding a non-loopback address
276
+ **requires** `ATLASSIAN_MCP_HTTP_TOKEN` (sent by clients as `Authorization: Bearer …`);
277
+ the server refuses to start otherwise. Terminate TLS at your proxy.
278
+ - **`GET /healthz`** is an unauthenticated liveness probe (returns `ok`) for proxies/load
279
+ balancers. Idle sessions are evicted after 1h.
280
+
281
+ **Repo context comes from the client, not the server's working directory.** Tools that
282
+ need a repo (the `git_*` tools, `get_dev_context`, `start_work`, `complete_work`, and
283
+ Bitbucket project/repo auto-detection) resolve it in this order: an explicit `repoPath`
284
+ argument → the client's **MCP workspace roots** (the server asks via `roots/list`, caches
285
+ per session, and refreshes on `notifications/roots/list_changed`) → the process cwd (stdio
286
+ only). So one shared HTTP server handles many worktrees: each client's own workspace drives
287
+ its calls. When a session exposes **several** roots (multiple worktrees), a tool with no
288
+ `repoPath` uses the first git-repo root; pass `repoPath` (an absolute path, or a worktree
289
+ name/basename that matches one of the roots) to target a specific worktree. For Bitbucket,
290
+ passing `projectKey`+`repoSlug` explicitly skips repo detection entirely. The repos must be
291
+ reachable on the server's host (the git tools run `git` locally).
292
+
293
+ Client config for an already-running HTTP server (Claude Code example):
294
+
295
+ ```bash
296
+ claude mcp add --transport http atlassian http://127.0.0.1:7337/mcp
297
+ ```
250
298
 
251
299
  ### Attachment decoding pipeline
252
300
 
@@ -254,28 +302,37 @@ The attachment tools (`jira_get_attachment`, `bitbucket_get_attachment`) decode
254
302
 
255
303
  | Input | What gets returned | How |
256
304
  | --- | --- | --- |
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 |
305
+ | 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) |
306
+ | Animated images (GIF/APNG/animated WebP) | N sampled frames as image content blocks | `ffmpeg` + native Go re-encode (default 6 frames @ 768 px) |
307
+ | 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
308
  | 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` |
309
+ | 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
310
  | 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 |
311
+ | Everything else (or oversized) | Auto-saved to a temp file; path is returned | `os.TempDir()` with `atlmcp-` prefix |
264
312
 
265
313
  Auto-saved files are periodically pruned by TTL and total-size quota — see *Environment overrides* below.
266
314
 
267
- ### Native dependencies
315
+ ### External tools (optional)
268
316
 
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.
317
+ Image and PDF-text decoding are pure Go and need nothing extra. The two pipelines that have no
318
+ pure-Go implementation shell out to external binaries:
319
+
320
+ - **`ffmpeg` + `ffprobe`** — video and animated-image frame sampling. The npm wrapper bundles
321
+ [`ffmpeg-static`](https://www.npmjs.com/package/ffmpeg-static) /
322
+ [`ffprobe-static`](https://www.npmjs.com/package/ffprobe-static) and injects their paths, so the
323
+ npx install path is zero-config. On the `go install` / Nix paths, install `ffmpeg` (it provides
324
+ `ffprobe`) or set the env vars below.
325
+ - **`pdftoppm` (poppler) or `mutool` (MuPDF)** — only needed to rasterize *scanned* PDFs that have no
326
+ extractable text. If neither is on `PATH`, such PDFs are saved to disk instead.
272
327
 
273
328
  ### Environment overrides
274
329
 
275
330
  | Variable | Purpose | Default |
276
331
  | --- | --- | --- |
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` |
332
+ | `ATLASSIAN_MCP_HTTP` | Run as a Streamable HTTP server instead of stdio. `1`/`true` `127.0.0.1:7337`; or set an explicit `host:port`. Same as `--http`. | unset (stdio) |
333
+ | `ATLASSIAN_MCP_HTTP_TOKEN` | Bearer token for HTTP mode. Optional on loopback binds; **required** on non-loopback binds. | unset |
334
+ | `ATLASSIAN_MCP_FFMPEG_PATH` | Path to `ffmpeg` binary. | npm: bundled `ffmpeg-static`; otherwise `ffmpeg` on `PATH` |
335
+ | `ATLASSIAN_MCP_FFPROBE_PATH` | Path to `ffprobe` binary. | npm: bundled `ffprobe-static`; otherwise `ffprobe` on `PATH` |
279
336
  | `ATLASSIAN_MCP_TMP_TTL_DAYS` | Auto-saved attachments older than this are pruned. | `7` |
280
337
  | `ATLASSIAN_MCP_TMP_MAX_BYTES` | Total-size quota for auto-saved attachments in `os.tmpdir()`. When exceeded, oldest are evicted. | `1073741824` (1 GB) |
281
338
 
@@ -287,23 +344,20 @@ This package is published to npm as `@stubbedev/atlassian-mcp`.
287
344
 
288
345
  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
346
 
290
- Automatic publish is configured in `.github/workflows/publish.yml` and runs when a new version tag is pushed.
347
+ On a pushed `v*` tag, `.github/workflows/publish.yml` cross-compiles the Go binary for 14
348
+ OS/arch targets, attaches them to a GitHub release, and publishes the npm wrapper (which
349
+ downloads the matching binary on install).
291
350
 
292
351
  Release flow:
293
352
 
294
353
  ```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
354
+ # choose one: patch | minor | major (also: npm run release:patch / :minor / :major)
355
+ npm version patch # bumps package.json, commits, tags vX.Y.Z
303
356
  git push origin HEAD --follow-tags
304
357
  ```
305
358
 
306
- GitHub Actions will publish the npm release from that pushed tag.
359
+ `flake.nix` reads its version from `package.json`, so the Nix package tracks the same bump
360
+ automatically. GitHub Actions builds + publishes from the pushed tag.
307
361
 
308
362
  - The workflow is configured for npm Trusted Publisher (OIDC), so no `NPM_TOKEN` secret is required
309
363
 
@@ -351,22 +405,22 @@ Paste the token as the `token` value under `bitbucket` in your config file.
351
405
 
352
406
  ## Development
353
407
 
408
+ The server is a single Go module at the repo root (no `src/` tree).
409
+
354
410
  ```bash
355
- # Watch mode — recompiles on file changes
356
- npm run dev
411
+ # Build the binary
412
+ go build -o atlassian-mcp .
413
+
414
+ # Run it
415
+ ./atlassian-mcp --config /path/to/config.json
357
416
 
358
- # Run the built server directly
359
- node dist/index.js
417
+ # Vet + unit tests
418
+ go vet ./...
419
+ go test ./...
360
420
 
361
421
  # Test the tool list
362
- echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node dist/index.js
422
+ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | ./atlassian-mcp
363
423
 
364
- # Quick release smoke check
424
+ # Quick release smoke check (build + tools/list validation)
365
425
  npm run smoke
366
426
  ```
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.1",
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
+ }