@atlassian-dc-mcp/common 0.12.2 → 0.14.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/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [0.14.0](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.13.0...v0.14.0) (2026-03-23)
7
+
8
+ **Note:** Version bump only for package @atlassian-dc-mcp/common
9
+
10
+
11
+
12
+
13
+
14
+ # [0.13.0](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.12.2...v0.13.0) (2026-03-21)
15
+
16
+
17
+ ### Features
18
+
19
+ * add ability to specify external config file to MCP, to provide configuration to MCP without specifying credentials in the file ([c97d380](https://github.com/b1ff/atlassian-dc-mcp/commit/c97d38004ebc7e233ea276a36c2e233536d2d0ca))
20
+
21
+
22
+
23
+
24
+
6
25
  ## [0.12.2](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.12.1...v0.12.2) (2026-03-21)
7
26
 
8
27
  **Note:** Version bump only for package @atlassian-dc-mcp/common
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=runtime-config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-config.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/runtime-config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ describe('runtime config loader', () => {
5
+ const originalEnv = process.env;
6
+ let tempDir;
7
+ beforeEach(() => {
8
+ jest.resetModules();
9
+ process.env = { ...originalEnv };
10
+ delete process.env.ATLASSIAN_DC_MCP_CONFIG_FILE;
11
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atlassian-dc-mcp-config-'));
12
+ });
13
+ afterEach(() => {
14
+ fs.rmSync(tempDir, { recursive: true, force: true });
15
+ });
16
+ afterAll(() => {
17
+ process.env = originalEnv;
18
+ });
19
+ it('loads an explicit shared config file', async () => {
20
+ const sharedConfigPath = path.join(tempDir, 'shared.env');
21
+ fs.writeFileSync(sharedConfigPath, 'JIRA_HOST=file-host\nJIRA_API_TOKEN=file-token\n');
22
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
23
+ const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../runtime-config.js');
24
+ initializeRuntimeConfig({ cwd: tempDir });
25
+ expect(getProductRuntimeConfig('jira')).toEqual({
26
+ host: 'file-host',
27
+ apiBasePath: undefined,
28
+ token: 'file-token',
29
+ defaultPageSize: 25,
30
+ });
31
+ });
32
+ it('throws when the explicit shared config file is missing', async () => {
33
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = path.join(tempDir, 'missing.env');
34
+ const { initializeRuntimeConfig } = await import('../runtime-config.js');
35
+ expect(() => initializeRuntimeConfig({ cwd: tempDir })).toThrow(`ATLASSIAN_DC_MCP_CONFIG_FILE points to a missing file: ${path.join(tempDir, 'missing.env')}`);
36
+ });
37
+ it('requires an absolute shared config file path', async () => {
38
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = 'relative/shared.env';
39
+ const { initializeRuntimeConfig } = await import('../runtime-config.js');
40
+ expect(() => initializeRuntimeConfig({ cwd: tempDir })).toThrow('ATLASSIAN_DC_MCP_CONFIG_FILE must be an absolute path: relative/shared.env');
41
+ });
42
+ it('keeps environment variables higher priority than file values', async () => {
43
+ const sharedConfigPath = path.join(tempDir, 'shared.env');
44
+ fs.writeFileSync(sharedConfigPath, 'JIRA_HOST=file-host\nJIRA_API_TOKEN=file-token\nJIRA_DEFAULT_PAGE_SIZE=50\n');
45
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
46
+ process.env.JIRA_API_TOKEN = 'env-token';
47
+ process.env.JIRA_DEFAULT_PAGE_SIZE = '10';
48
+ const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../runtime-config.js');
49
+ initializeRuntimeConfig({ cwd: tempDir });
50
+ expect(getProductRuntimeConfig('jira')).toEqual({
51
+ host: 'file-host',
52
+ apiBasePath: undefined,
53
+ token: 'env-token',
54
+ defaultPageSize: 10,
55
+ });
56
+ });
57
+ it('falls back to the cwd .env file when no explicit shared file is configured', async () => {
58
+ fs.writeFileSync(path.join(tempDir, '.env'), 'CONFLUENCE_HOST=cwd-host\nCONFLUENCE_API_TOKEN=cwd-token\nCONFLUENCE_DEFAULT_PAGE_SIZE=30\n');
59
+ const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../runtime-config.js');
60
+ initializeRuntimeConfig({ cwd: tempDir });
61
+ expect(getProductRuntimeConfig('confluence')).toEqual({
62
+ host: 'cwd-host',
63
+ apiBasePath: undefined,
64
+ token: 'cwd-token',
65
+ defaultPageSize: 30,
66
+ });
67
+ });
68
+ it('refreshes the cached file when its mtime changes', async () => {
69
+ const sharedConfigPath = path.join(tempDir, 'shared.env');
70
+ fs.writeFileSync(sharedConfigPath, 'BITBUCKET_HOST=file-host\nBITBUCKET_API_TOKEN=token-a\n');
71
+ const initialTime = new Date('2026-01-01T00:00:00.000Z');
72
+ fs.utimesSync(sharedConfigPath, initialTime, initialTime);
73
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
74
+ const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../runtime-config.js');
75
+ initializeRuntimeConfig({ cwd: tempDir });
76
+ expect(getProductRuntimeConfig('bitbucket').token).toBe('token-a');
77
+ fs.writeFileSync(sharedConfigPath, 'BITBUCKET_HOST=file-host\nBITBUCKET_API_TOKEN=token-b\n');
78
+ const updatedTime = new Date('2026-01-01T00:00:01.000Z');
79
+ fs.utimesSync(sharedConfigPath, updatedTime, updatedTime);
80
+ expect(getProductRuntimeConfig('bitbucket').token).toBe('token-b');
81
+ });
82
+ });
83
+ //# sourceMappingURL=runtime-config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-config.test.js","sourceRoot":"","sources":["../../src/__tests__/runtime-config.test.ts"],"names":[],"mappings":"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"}
package/build/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import 'dotenv/config';
2
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  export * from './api-error-handler.js';
3
+ export * from './runtime-config.js';
4
4
  export declare const formatToolResponse: (result: unknown) => {
5
5
  content: {
6
6
  type: "text";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,cAAc,wBAAwB,CAAA;AAGtC,eAAO,MAAM,kBAAkB,GAAI,QAAQ,OAAO;;;;;CAKhD,CAAC;AAGH,eAAO,MAAM,WAAW,GAAI,OAAO,KAAK,UAGvC,CAAC;AAGF,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,aAKA;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,SAAS,sBAkCpD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAC;AAGpC,eAAO,MAAM,kBAAkB,GAAI,QAAQ,OAAO;;;;;CAKhD,CAAC;AAGH,eAAO,MAAM,WAAW,GAAI,OAAO,KAAK,UAGvC,CAAC;AAGF,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,aAKA;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,SAAS,sBAkCpD"}
package/build/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import 'dotenv/config';
2
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
3
  export * from './api-error-handler.js';
4
+ export * from './runtime-config.js';
5
5
  // Helper function to format tool responses
6
6
  export const formatToolResponse = (result) => ({
7
7
  content: [{
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,cAAc,wBAAwB,CAAA;AAEtC,2CAA2C;AAC3C,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,MAAe,EAAE,EAAE,CAAC,CAAC;IACtD,OAAO,EAAE,CAAC;YACR,IAAI,EAAE,MAAe;YACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;SAC7B,CAAC;CACH,CAAC,CAAC;AAEH,uBAAuB;AACvB,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAY,EAAE,EAAE;IAC1C,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;IACtC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;AAEF,sCAAsC;AACtC,MAAM,UAAU,eAAe,CAAC,OAG/B;IACC,OAAO,IAAI,SAAS,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAiB;IACnD,0DAA0D;IAC1D,yBAAyB;IACzB,iDAAiD;IACjD,EAAE;IACF,wCAAwC;IACxC,0DAA0D;IAC1D,qCAAqC;IACrC,MAAM;IACN,EAAE;IACF,8CAA8C;IAC9C,kDAAkD;IAClD,MAAM;IACN,EAAE;IACF,mDAAmD;IACnD,sCAAsC;IACtC,2CAA2C;IAC3C,kDAAkD;IAClD,2CAA2C;IAC3C,4CAA4C;IAC5C,mBAAmB;IACnB,uBAAuB;IACvB,gBAAgB;IAChB,QAAQ;IACR,EAAE;IACF,uDAAuD;IACvD,qBAAqB;IACrB,QAAQ;IACR,MAAM;IAGN,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAC;AAEpC,2CAA2C;AAC3C,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,MAAe,EAAE,EAAE,CAAC,CAAC;IACtD,OAAO,EAAE,CAAC;YACR,IAAI,EAAE,MAAe;YACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;SAC7B,CAAC;CACH,CAAC,CAAC;AAEH,uBAAuB;AACvB,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAY,EAAE,EAAE;IAC1C,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;IACtC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;AAEF,sCAAsC;AACtC,MAAM,UAAU,eAAe,CAAC,OAG/B;IACC,OAAO,IAAI,SAAS,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAiB;IACnD,0DAA0D;IAC1D,yBAAyB;IACzB,iDAAiD;IACjD,EAAE;IACF,wCAAwC;IACxC,0DAA0D;IAC1D,qCAAqC;IACrC,MAAM;IACN,EAAE;IACF,8CAA8C;IAC9C,kDAAkD;IAClD,MAAM;IACN,EAAE;IACF,mDAAmD;IACnD,sCAAsC;IACtC,2CAA2C;IAC3C,kDAAkD;IAClD,2CAA2C;IAC3C,4CAA4C;IAC5C,mBAAmB;IACnB,uBAAuB;IACvB,gBAAgB;IAChB,QAAQ;IACR,EAAE;IACF,uDAAuD;IACvD,qBAAqB;IACrB,QAAQ;IACR,MAAM;IAGN,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,14 @@
1
+ export declare const ATLASSIAN_DC_MCP_CONFIG_FILE_ENV_VAR = "ATLASSIAN_DC_MCP_CONFIG_FILE";
2
+ export type AtlassianProduct = 'jira' | 'confluence' | 'bitbucket';
3
+ export type ProductRuntimeConfig = {
4
+ host?: string;
5
+ apiBasePath?: string;
6
+ token?: string;
7
+ defaultPageSize: number;
8
+ };
9
+ export declare function initializeRuntimeConfig(options?: {
10
+ cwd?: string;
11
+ }): void;
12
+ export declare function getProductRuntimeConfig(product: AtlassianProduct): ProductRuntimeConfig;
13
+ export declare function validateProductRuntimeConfig(product: AtlassianProduct): string[];
14
+ //# sourceMappingURL=runtime-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-config.d.ts","sourceRoot":"","sources":["../src/runtime-config.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,oCAAoC,iCAAiC,CAAC;AAEnF,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,CAAC;AAoBnE,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AA+GF,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,QAGjE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,gBAAgB,GAAG,oBAAoB,CAUvF;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAchF"}
@@ -0,0 +1,123 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import dotenv from 'dotenv';
4
+ const FALLBACK_PAGE_SIZE = 25;
5
+ export const ATLASSIAN_DC_MCP_CONFIG_FILE_ENV_VAR = 'ATLASSIAN_DC_MCP_CONFIG_FILE';
6
+ const PRODUCT_METADATA = {
7
+ jira: {
8
+ hostKey: 'JIRA_HOST',
9
+ apiBasePathKey: 'JIRA_API_BASE_PATH',
10
+ tokenKey: 'JIRA_API_TOKEN',
11
+ defaultPageSizeKey: 'JIRA_DEFAULT_PAGE_SIZE',
12
+ },
13
+ confluence: {
14
+ hostKey: 'CONFLUENCE_HOST',
15
+ apiBasePathKey: 'CONFLUENCE_API_BASE_PATH',
16
+ tokenKey: 'CONFLUENCE_API_TOKEN',
17
+ defaultPageSizeKey: 'CONFLUENCE_DEFAULT_PAGE_SIZE',
18
+ },
19
+ bitbucket: {
20
+ hostKey: 'BITBUCKET_HOST',
21
+ apiBasePathKey: 'BITBUCKET_API_BASE_PATH',
22
+ tokenKey: 'BITBUCKET_API_TOKEN',
23
+ defaultPageSizeKey: 'BITBUCKET_DEFAULT_PAGE_SIZE',
24
+ },
25
+ };
26
+ const runtimeConfigState = {
27
+ cwd: process.cwd(),
28
+ };
29
+ function parsePositiveInteger(value) {
30
+ if (!value) {
31
+ return undefined;
32
+ }
33
+ const trimmed = value.trim();
34
+ if (!/^\d+$/.test(trimmed)) {
35
+ return undefined;
36
+ }
37
+ const parsed = Number.parseInt(trimmed, 10);
38
+ return parsed > 0 ? parsed : undefined;
39
+ }
40
+ function getNonEmptyValue(value) {
41
+ if (!value) {
42
+ return undefined;
43
+ }
44
+ const trimmed = value.trim();
45
+ return trimmed.length > 0 ? trimmed : undefined;
46
+ }
47
+ function getExplicitConfigFilePath() {
48
+ const filePath = getNonEmptyValue(process.env[ATLASSIAN_DC_MCP_CONFIG_FILE_ENV_VAR]);
49
+ if (!filePath) {
50
+ return undefined;
51
+ }
52
+ if (!path.isAbsolute(filePath)) {
53
+ throw new Error(`${ATLASSIAN_DC_MCP_CONFIG_FILE_ENV_VAR} must be an absolute path: ${filePath}`);
54
+ }
55
+ return filePath;
56
+ }
57
+ function clearCachedFileIfNeeded(filePath) {
58
+ if (runtimeConfigState.cachedFile?.filePath === filePath) {
59
+ runtimeConfigState.cachedFile = undefined;
60
+ }
61
+ }
62
+ function getParsedFileEnvironment() {
63
+ const explicitFilePath = getExplicitConfigFilePath();
64
+ const filePath = explicitFilePath ?? path.join(runtimeConfigState.cwd, '.env');
65
+ let stats;
66
+ try {
67
+ stats = fs.statSync(filePath);
68
+ }
69
+ catch (error) {
70
+ clearCachedFileIfNeeded(filePath);
71
+ if (error.code === 'ENOENT' && !explicitFilePath) {
72
+ return {};
73
+ }
74
+ if (error.code === 'ENOENT' && explicitFilePath) {
75
+ throw new Error(`${ATLASSIAN_DC_MCP_CONFIG_FILE_ENV_VAR} points to a missing file: ${filePath}`);
76
+ }
77
+ throw error;
78
+ }
79
+ const cachedFile = runtimeConfigState.cachedFile;
80
+ if (cachedFile && cachedFile.filePath === filePath && cachedFile.mtimeMs === stats.mtimeMs) {
81
+ return cachedFile.values;
82
+ }
83
+ const values = dotenv.parse(fs.readFileSync(filePath));
84
+ runtimeConfigState.cachedFile = {
85
+ filePath,
86
+ mtimeMs: stats.mtimeMs,
87
+ values,
88
+ };
89
+ return values;
90
+ }
91
+ function getMergedEnvironment() {
92
+ return {
93
+ ...getParsedFileEnvironment(),
94
+ ...process.env,
95
+ };
96
+ }
97
+ export function initializeRuntimeConfig(options) {
98
+ runtimeConfigState.cwd = options?.cwd ?? process.cwd();
99
+ getParsedFileEnvironment();
100
+ }
101
+ export function getProductRuntimeConfig(product) {
102
+ const environment = getMergedEnvironment();
103
+ const metadata = PRODUCT_METADATA[product];
104
+ return {
105
+ host: getNonEmptyValue(environment[metadata.hostKey]),
106
+ apiBasePath: getNonEmptyValue(environment[metadata.apiBasePathKey]),
107
+ token: getNonEmptyValue(environment[metadata.tokenKey]),
108
+ defaultPageSize: parsePositiveInteger(environment[metadata.defaultPageSizeKey]) ?? FALLBACK_PAGE_SIZE,
109
+ };
110
+ }
111
+ export function validateProductRuntimeConfig(product) {
112
+ const metadata = PRODUCT_METADATA[product];
113
+ const config = getProductRuntimeConfig(product);
114
+ const missing = [];
115
+ if (!config.token) {
116
+ missing.push(metadata.tokenKey);
117
+ }
118
+ if (!config.host && !config.apiBasePath) {
119
+ missing.push(`${metadata.hostKey} or ${metadata.apiBasePathKey}`);
120
+ }
121
+ return missing;
122
+ }
123
+ //# sourceMappingURL=runtime-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-config.js","sourceRoot":"","sources":["../src/runtime-config.ts"],"names":[],"mappings":"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"}
package/jest.config.js ADDED
@@ -0,0 +1,19 @@
1
+ export default {
2
+ preset: 'ts-jest/presets/default-esm',
3
+ extensionsToTreatAsEsm: ['.ts'],
4
+ moduleNameMapper: {
5
+ '^(\\.{1,2}/.*)\\.js$': '$1',
6
+ },
7
+ transform: {
8
+ '^.+\\.ts$': ['ts-jest', {
9
+ useESM: true,
10
+ }],
11
+ },
12
+ testEnvironment: 'node',
13
+ testMatch: ['**/__tests__/**/*.test.ts'],
14
+ collectCoverageFrom: [
15
+ 'src/**/*.ts',
16
+ '!src/**/*.d.ts',
17
+ '!src/**/index.ts',
18
+ ],
19
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlassian-dc-mcp/common",
3
- "version": "0.12.2",
3
+ "version": "0.14.0",
4
4
  "main": "build/index.js",
5
5
  "type": "module",
6
6
  "private": false,
@@ -22,5 +22,5 @@
22
22
  "publishConfig": {
23
23
  "access": "public"
24
24
  },
25
- "gitHead": "6488590c1832714d510cc4a4f5f3d9e8e38f3b09"
25
+ "gitHead": "cddfcbf3cb1404e69957277fd988c28e08700fae"
26
26
  }
@@ -0,0 +1,116 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ describe('runtime config loader', () => {
6
+ const originalEnv = process.env;
7
+ let tempDir: string;
8
+
9
+ beforeEach(() => {
10
+ jest.resetModules();
11
+ process.env = { ...originalEnv };
12
+ delete process.env.ATLASSIAN_DC_MCP_CONFIG_FILE;
13
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atlassian-dc-mcp-config-'));
14
+ });
15
+
16
+ afterEach(() => {
17
+ fs.rmSync(tempDir, { recursive: true, force: true });
18
+ });
19
+
20
+ afterAll(() => {
21
+ process.env = originalEnv;
22
+ });
23
+
24
+ it('loads an explicit shared config file', async () => {
25
+ const sharedConfigPath = path.join(tempDir, 'shared.env');
26
+ fs.writeFileSync(sharedConfigPath, 'JIRA_HOST=file-host\nJIRA_API_TOKEN=file-token\n');
27
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
28
+
29
+ const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../runtime-config.js');
30
+
31
+ initializeRuntimeConfig({ cwd: tempDir });
32
+
33
+ expect(getProductRuntimeConfig('jira')).toEqual({
34
+ host: 'file-host',
35
+ apiBasePath: undefined,
36
+ token: 'file-token',
37
+ defaultPageSize: 25,
38
+ });
39
+ });
40
+
41
+ it('throws when the explicit shared config file is missing', async () => {
42
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = path.join(tempDir, 'missing.env');
43
+
44
+ const { initializeRuntimeConfig } = await import('../runtime-config.js');
45
+
46
+ expect(() => initializeRuntimeConfig({ cwd: tempDir })).toThrow(
47
+ `ATLASSIAN_DC_MCP_CONFIG_FILE points to a missing file: ${path.join(tempDir, 'missing.env')}`,
48
+ );
49
+ });
50
+
51
+ it('requires an absolute shared config file path', async () => {
52
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = 'relative/shared.env';
53
+
54
+ const { initializeRuntimeConfig } = await import('../runtime-config.js');
55
+
56
+ expect(() => initializeRuntimeConfig({ cwd: tempDir })).toThrow(
57
+ 'ATLASSIAN_DC_MCP_CONFIG_FILE must be an absolute path: relative/shared.env',
58
+ );
59
+ });
60
+
61
+ it('keeps environment variables higher priority than file values', async () => {
62
+ const sharedConfigPath = path.join(tempDir, 'shared.env');
63
+ fs.writeFileSync(sharedConfigPath, 'JIRA_HOST=file-host\nJIRA_API_TOKEN=file-token\nJIRA_DEFAULT_PAGE_SIZE=50\n');
64
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
65
+ process.env.JIRA_API_TOKEN = 'env-token';
66
+ process.env.JIRA_DEFAULT_PAGE_SIZE = '10';
67
+
68
+ const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../runtime-config.js');
69
+
70
+ initializeRuntimeConfig({ cwd: tempDir });
71
+
72
+ expect(getProductRuntimeConfig('jira')).toEqual({
73
+ host: 'file-host',
74
+ apiBasePath: undefined,
75
+ token: 'env-token',
76
+ defaultPageSize: 10,
77
+ });
78
+ });
79
+
80
+ it('falls back to the cwd .env file when no explicit shared file is configured', async () => {
81
+ fs.writeFileSync(
82
+ path.join(tempDir, '.env'),
83
+ 'CONFLUENCE_HOST=cwd-host\nCONFLUENCE_API_TOKEN=cwd-token\nCONFLUENCE_DEFAULT_PAGE_SIZE=30\n',
84
+ );
85
+
86
+ const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../runtime-config.js');
87
+
88
+ initializeRuntimeConfig({ cwd: tempDir });
89
+
90
+ expect(getProductRuntimeConfig('confluence')).toEqual({
91
+ host: 'cwd-host',
92
+ apiBasePath: undefined,
93
+ token: 'cwd-token',
94
+ defaultPageSize: 30,
95
+ });
96
+ });
97
+
98
+ it('refreshes the cached file when its mtime changes', async () => {
99
+ const sharedConfigPath = path.join(tempDir, 'shared.env');
100
+ fs.writeFileSync(sharedConfigPath, 'BITBUCKET_HOST=file-host\nBITBUCKET_API_TOKEN=token-a\n');
101
+ const initialTime = new Date('2026-01-01T00:00:00.000Z');
102
+ fs.utimesSync(sharedConfigPath, initialTime, initialTime);
103
+ process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
104
+
105
+ const { getProductRuntimeConfig, initializeRuntimeConfig } = await import('../runtime-config.js');
106
+
107
+ initializeRuntimeConfig({ cwd: tempDir });
108
+ expect(getProductRuntimeConfig('bitbucket').token).toBe('token-a');
109
+
110
+ fs.writeFileSync(sharedConfigPath, 'BITBUCKET_HOST=file-host\nBITBUCKET_API_TOKEN=token-b\n');
111
+ const updatedTime = new Date('2026-01-01T00:00:01.000Z');
112
+ fs.utimesSync(sharedConfigPath, updatedTime, updatedTime);
113
+
114
+ expect(getProductRuntimeConfig('bitbucket').token).toBe('token-b');
115
+ });
116
+ });
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
- import 'dotenv/config';
2
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
4
  export * from './api-error-handler.js'
5
+ export * from './runtime-config.js';
6
6
 
7
7
  // Helper function to format tool responses
8
8
  export const formatToolResponse = (result: unknown) => ({
@@ -0,0 +1,176 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import dotenv from 'dotenv';
4
+
5
+ const FALLBACK_PAGE_SIZE = 25;
6
+
7
+ export const ATLASSIAN_DC_MCP_CONFIG_FILE_ENV_VAR = 'ATLASSIAN_DC_MCP_CONFIG_FILE';
8
+
9
+ export type AtlassianProduct = 'jira' | 'confluence' | 'bitbucket';
10
+
11
+ type ProductMetadata = {
12
+ hostKey: string;
13
+ apiBasePathKey: string;
14
+ tokenKey: string;
15
+ defaultPageSizeKey: string;
16
+ };
17
+
18
+ type ParsedEnvironment = Record<string, string>;
19
+
20
+ type RuntimeConfigState = {
21
+ cwd: string;
22
+ cachedFile?: {
23
+ filePath: string;
24
+ mtimeMs: number;
25
+ values: ParsedEnvironment;
26
+ };
27
+ };
28
+
29
+ export type ProductRuntimeConfig = {
30
+ host?: string;
31
+ apiBasePath?: string;
32
+ token?: string;
33
+ defaultPageSize: number;
34
+ };
35
+
36
+ const PRODUCT_METADATA: Record<AtlassianProduct, ProductMetadata> = {
37
+ jira: {
38
+ hostKey: 'JIRA_HOST',
39
+ apiBasePathKey: 'JIRA_API_BASE_PATH',
40
+ tokenKey: 'JIRA_API_TOKEN',
41
+ defaultPageSizeKey: 'JIRA_DEFAULT_PAGE_SIZE',
42
+ },
43
+ confluence: {
44
+ hostKey: 'CONFLUENCE_HOST',
45
+ apiBasePathKey: 'CONFLUENCE_API_BASE_PATH',
46
+ tokenKey: 'CONFLUENCE_API_TOKEN',
47
+ defaultPageSizeKey: 'CONFLUENCE_DEFAULT_PAGE_SIZE',
48
+ },
49
+ bitbucket: {
50
+ hostKey: 'BITBUCKET_HOST',
51
+ apiBasePathKey: 'BITBUCKET_API_BASE_PATH',
52
+ tokenKey: 'BITBUCKET_API_TOKEN',
53
+ defaultPageSizeKey: 'BITBUCKET_DEFAULT_PAGE_SIZE',
54
+ },
55
+ };
56
+
57
+ const runtimeConfigState: RuntimeConfigState = {
58
+ cwd: process.cwd(),
59
+ };
60
+
61
+ function parsePositiveInteger(value: string | undefined): number | undefined {
62
+ if (!value) {
63
+ return undefined;
64
+ }
65
+
66
+ const trimmed = value.trim();
67
+ if (!/^\d+$/.test(trimmed)) {
68
+ return undefined;
69
+ }
70
+
71
+ const parsed = Number.parseInt(trimmed, 10);
72
+ return parsed > 0 ? parsed : undefined;
73
+ }
74
+
75
+ function getNonEmptyValue(value: string | undefined): string | undefined {
76
+ if (!value) {
77
+ return undefined;
78
+ }
79
+
80
+ const trimmed = value.trim();
81
+ return trimmed.length > 0 ? trimmed : undefined;
82
+ }
83
+
84
+ function getExplicitConfigFilePath(): string | undefined {
85
+ const filePath = getNonEmptyValue(process.env[ATLASSIAN_DC_MCP_CONFIG_FILE_ENV_VAR]);
86
+ if (!filePath) {
87
+ return undefined;
88
+ }
89
+
90
+ if (!path.isAbsolute(filePath)) {
91
+ throw new Error(`${ATLASSIAN_DC_MCP_CONFIG_FILE_ENV_VAR} must be an absolute path: ${filePath}`);
92
+ }
93
+
94
+ return filePath;
95
+ }
96
+
97
+ function clearCachedFileIfNeeded(filePath: string) {
98
+ if (runtimeConfigState.cachedFile?.filePath === filePath) {
99
+ runtimeConfigState.cachedFile = undefined;
100
+ }
101
+ }
102
+
103
+ function getParsedFileEnvironment(): ParsedEnvironment {
104
+ const explicitFilePath = getExplicitConfigFilePath();
105
+ const filePath = explicitFilePath ?? path.join(runtimeConfigState.cwd, '.env');
106
+
107
+ let stats: fs.Stats;
108
+ try {
109
+ stats = fs.statSync(filePath);
110
+ } catch (error) {
111
+ clearCachedFileIfNeeded(filePath);
112
+
113
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' && !explicitFilePath) {
114
+ return {};
115
+ }
116
+
117
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' && explicitFilePath) {
118
+ throw new Error(`${ATLASSIAN_DC_MCP_CONFIG_FILE_ENV_VAR} points to a missing file: ${filePath}`);
119
+ }
120
+
121
+ throw error;
122
+ }
123
+
124
+ const cachedFile = runtimeConfigState.cachedFile;
125
+ if (cachedFile && cachedFile.filePath === filePath && cachedFile.mtimeMs === stats.mtimeMs) {
126
+ return cachedFile.values;
127
+ }
128
+
129
+ const values = dotenv.parse(fs.readFileSync(filePath));
130
+ runtimeConfigState.cachedFile = {
131
+ filePath,
132
+ mtimeMs: stats.mtimeMs,
133
+ values,
134
+ };
135
+ return values;
136
+ }
137
+
138
+ function getMergedEnvironment(): NodeJS.ProcessEnv {
139
+ return {
140
+ ...getParsedFileEnvironment(),
141
+ ...process.env,
142
+ };
143
+ }
144
+
145
+ export function initializeRuntimeConfig(options?: { cwd?: string }) {
146
+ runtimeConfigState.cwd = options?.cwd ?? process.cwd();
147
+ getParsedFileEnvironment();
148
+ }
149
+
150
+ export function getProductRuntimeConfig(product: AtlassianProduct): ProductRuntimeConfig {
151
+ const environment = getMergedEnvironment();
152
+ const metadata = PRODUCT_METADATA[product];
153
+
154
+ return {
155
+ host: getNonEmptyValue(environment[metadata.hostKey]),
156
+ apiBasePath: getNonEmptyValue(environment[metadata.apiBasePathKey]),
157
+ token: getNonEmptyValue(environment[metadata.tokenKey]),
158
+ defaultPageSize: parsePositiveInteger(environment[metadata.defaultPageSizeKey]) ?? FALLBACK_PAGE_SIZE,
159
+ };
160
+ }
161
+
162
+ export function validateProductRuntimeConfig(product: AtlassianProduct): string[] {
163
+ const metadata = PRODUCT_METADATA[product];
164
+ const config = getProductRuntimeConfig(product);
165
+ const missing: string[] = [];
166
+
167
+ if (!config.token) {
168
+ missing.push(metadata.tokenKey);
169
+ }
170
+
171
+ if (!config.host && !config.apiBasePath) {
172
+ missing.push(`${metadata.hostKey} or ${metadata.apiBasePathKey}`);
173
+ }
174
+
175
+ return missing;
176
+ }