@mcp-abap-adt/proxy 1.5.0 → 1.6.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
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.6.0] - 2026-06-04
11
+
12
+ ### Added
13
+ - **Environment-variable interpolation in config + `.env` support.** Config string values (and `--header` values) may now contain `${VAR}` / `${VAR:-default}` placeholders, resolved from `process.env` and an explicitly-pointed `.env` file. This makes `defaultHeaders` the per-user auth-injection point for ABAP credentials (`x-sap-login` / `x-sap-password`) on shared servers like `cloud-llm-hub` — **without** storing secrets as plaintext in the YAML. (#4, #5)
14
+ - `.env` source is explicit only: `envFile: <path>` in the config (resolved relative to the config file's directory) or `--env-file <path>` on the command line (overrides `envFile`). There is no `cwd` auto-discovery.
15
+ - Resolution priority: `process.env` → `.env` → `${VAR:-default}`. `process.env` wins over the `.env` file.
16
+ - **Fail-fast:** a `${VAR}` without a default that cannot be resolved aborts startup with an error naming the variable and the field it came from, so a request never silently goes out with an empty credential.
17
+ - `Authorization` remains proxy-managed (destination JWT) and cannot be set via `defaultHeaders`.
18
+
10
19
  ## [1.5.0] - 2026-06-02
11
20
 
12
21
  ### Added
package/README.md CHANGED
@@ -98,7 +98,7 @@ For detailed setup instructions for Cline and GitHub Copilot, see the **[Client
98
98
 
99
99
  MCP clients like Cline and Claude Code cannot set arbitrary request headers. Use default headers to inject SAP-specific headers (e.g. `x-sap-destination`, `x-sap-client`) that the target MCP server requires.
100
100
 
101
- Client-supplied headers always take precedence over defaults.
101
+ Precedence: client-supplied request headers always win over `defaultHeaders`. `Authorization` is the exception — it is always managed by the proxy (replaced with the destination JWT) and **cannot** be set via `defaultHeaders`.
102
102
 
103
103
  Via YAML config (`defaultHeaders` map):
104
104
  ```yaml
@@ -109,6 +109,28 @@ defaultHeaders:
109
109
  x-sap-client: "100"
110
110
  ```
111
111
 
112
+ **Per-user ABAP credentials.** `defaultHeaders` is the supported place to supply your own SAP login/password for on-premise / `NoAuthentication` destinations. The upstream `cloud-llm-hub` is a **shared** server with no default service user, so it expects each caller's own `x-sap-login` / `x-sap-password` on every request. Because the proxy runs **locally on each user's machine**, it carries *your* identity.
113
+
114
+ Do not hardcode secrets. Reference environment variables with `${VAR}` (or `${VAR:-default}`); values are resolved from `process.env` or an explicitly-pointed `.env` file:
115
+
116
+ ```yaml
117
+ btpDestination: mcp
118
+ envFile: secrets.env # resolved relative to this config file's dir
119
+ defaultHeaders:
120
+ x-sap-destination: S4HANA_E19
121
+ x-sap-login: ${SAP_USER}
122
+ x-sap-password: ${SAP_PASSWORD}
123
+ ```
124
+
125
+ `secrets.env` (user-local, `chmod 600`, never committed):
126
+
127
+ ```dotenv
128
+ SAP_USER=MY_SAP_USER
129
+ SAP_PASSWORD=my-sap-password
130
+ ```
131
+
132
+ Resolution order is `process.env` → `.env` → `${VAR:-default}`. `process.env` wins; an unresolved `${VAR}` without a default fails the proxy at startup. Override the `.env` path at launch with `--env-file <path>`. This closes both auth layers from one local config: the **service layer** (`Authorization: Bearer <JWT>`, from the destination service key) and the **ABAP layer** (`x-sap-login` / `x-sap-password`).
133
+
112
134
  Via CLI (`--header`, repeatable):
113
135
  ```bash
114
136
  mcp-abap-adt-proxy --btp=mcp --url=https://example.com \
@@ -56,9 +56,13 @@ Options:
56
56
  Useful for direct OData service testing or non-standard MCP URLs
57
57
  --config=<file>, -c Load configuration from YAML or JSON file
58
58
  Alternative to command-line parameters
59
+ --env-file=<path> Load a .env file for ${VAR} interpolation in config/headers
60
+ Overrides the config's "envFile" field
59
61
  --unsafe Enable file-based session storage (persists tokens to disk)
60
62
  --header key=value Add default header to every forwarded request (repeatable)
63
+ Values support ${VAR} / ${VAR:-default} interpolation
61
64
  Example: --header x-sap-destination=S4HANA
65
+ Example: --header x-sap-password=${SAP_PASSWORD}
62
66
  --help, -h Show this help message
63
67
  --version, -v Show version number
64
68
 
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IAEjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAGxC,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAG/B,OAAO,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAyB3D;AAwED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,mBAAmB,CAQrB;AAgBD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAYhC;AA4ID;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAElD;AAUD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG;IACnD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAoDA"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAYH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IAEjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAGxC,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAG/B,OAAO,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAoC3D;AAwED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,mBAAmB,CAQrB;AAkCD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAYhC;AAmJD;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAElD;AAUD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG;IACnD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAoDA"}
@@ -44,6 +44,7 @@ exports.validateConfig = validateConfig;
44
44
  const fs = __importStar(require("node:fs"));
45
45
  const path = __importStar(require("node:path"));
46
46
  const yaml = __importStar(require("js-yaml"));
47
+ const envInterpolation_js_1 = require("./envInterpolation.js");
47
48
  /**
48
49
  * Load configuration from file if --config is provided, otherwise from environment variables and command line
49
50
  * Supports both JSON and YAML formats
@@ -64,7 +65,12 @@ function loadConfig(configPath) {
64
65
  throw new Error(`Config file not found: ${finalConfigPath}`);
65
66
  }
66
67
  const fileConfig = loadConfigFile(finalConfigPath);
67
- const base = applyDefaults(fileConfig);
68
+ const envFilePath = resolveEnvFilePath(fileConfig, finalConfigPath);
69
+ const envFileMap = envFilePath ? (0, envInterpolation_js_1.loadEnvFile)(envFilePath) : {};
70
+ const lookup = (0, envInterpolation_js_1.buildLookup)(envFileMap);
71
+ const interpolated = (0, envInterpolation_js_1.interpolateConfig)(fileConfig, lookup);
72
+ delete interpolated.envFile;
73
+ const base = applyDefaults(interpolated);
68
74
  const cli = readCliOverrides();
69
75
  return mergeCliOverrides(base, cli, finalConfigPath);
70
76
  }
@@ -155,6 +161,21 @@ function loadConfigFile(filePath) {
155
161
  return JSON.parse(configContent);
156
162
  }
157
163
  }
164
+ /**
165
+ * Resolve the .env path for the file-config path: --env-file wins (relative to
166
+ * cwd); otherwise the YAML `envFile` field, resolved relative to the config
167
+ * file's directory. Returns undefined when neither is set.
168
+ */
169
+ function resolveEnvFilePath(rawConfig, configPath) {
170
+ const cliEnvFile = getArgValue('--env-file');
171
+ if (cliEnvFile !== undefined)
172
+ return path.resolve(cliEnvFile);
173
+ const yamlEnvFile = rawConfig.envFile;
174
+ if (typeof yamlEnvFile === 'string') {
175
+ return path.resolve(path.dirname(configPath), yamlEnvFile);
176
+ }
177
+ return undefined;
178
+ }
158
179
  /**
159
180
  * Load raw config from file for transport field extraction
160
181
  */
@@ -210,17 +231,20 @@ function loadFromEnv() {
210
231
  const browserAuthPort = browserAuthPortStr
211
232
  ? parseInt(browserAuthPortStr, 10)
212
233
  : undefined;
213
- // Parse repeatable --header key=value arguments
234
+ // Parse repeatable --header key=value arguments, interpolating ${VAR}
214
235
  const headerArgs = getAllArgValues('--header');
215
236
  let defaultHeaders;
216
237
  if (headerArgs.length > 0) {
238
+ const cliEnvFile = getArgValue('--env-file');
239
+ const envFileMap = cliEnvFile ? (0, envInterpolation_js_1.loadEnvFile)(path.resolve(cliEnvFile)) : {};
240
+ const lookup = (0, envInterpolation_js_1.buildLookup)(envFileMap);
217
241
  defaultHeaders = {};
218
242
  for (const arg of headerArgs) {
219
243
  const eqIndex = arg.indexOf('=');
220
244
  if (eqIndex > 0) {
221
245
  const key = arg.substring(0, eqIndex).toLowerCase();
222
- const value = arg.substring(eqIndex + 1);
223
- defaultHeaders[key] = value;
246
+ const rawValue = arg.substring(eqIndex + 1);
247
+ defaultHeaders[key] = (0, envInterpolation_js_1.interpolateString)(rawValue, lookup, `--header ${key}`);
224
248
  }
225
249
  }
226
250
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Environment-variable interpolation for proxy configuration.
3
+ * Supports ${VAR} and ${VAR:-default} in string values.
4
+ */
5
+ /**
6
+ * Resolve ${VAR} / ${VAR:-default} placeholders in a single string.
7
+ * Empty-string follows bash `:-` semantics: an empty value falls back to the
8
+ * default when one is given. A ${VAR} without a default that resolves to
9
+ * undefined throws, naming the variable and the field it came from.
10
+ */
11
+ export declare function interpolateString(input: string, lookup: (key: string) => string | undefined, fieldPath: string): string;
12
+ /**
13
+ * Recursively interpolate all string values in a parsed config object.
14
+ * Objects/arrays are walked; non-string scalars are returned unchanged.
15
+ * The field path (e.g. `defaultHeaders.x-sap-password`) is threaded through for
16
+ * error messages.
17
+ */
18
+ export declare function interpolateConfig(value: unknown, lookup: (key: string) => string | undefined, path?: string): unknown;
19
+ /**
20
+ * Parse a .env file into a flat map. Throws if the path is given but missing —
21
+ * a specified-yet-absent secret source is a configuration error, not a no-op.
22
+ */
23
+ export declare function loadEnvFile(envFilePath: string): Record<string, string>;
24
+ /**
25
+ * Build a lookup over process.env (highest priority) then the parsed .env map.
26
+ */
27
+ export declare function buildLookup(envFileMap: Record<string, string>): (key: string) => string | undefined;
28
+ //# sourceMappingURL=envInterpolation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envInterpolation.d.ts","sourceRoot":"","sources":["../../src/lib/envInterpolation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,EAC3C,SAAS,EAAE,MAAM,GAChB,MAAM,CAiBR;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,EAC3C,IAAI,SAAK,GACR,OAAO,CAiBT;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKvE;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAGrC"}
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ /**
3
+ * Environment-variable interpolation for proxy configuration.
4
+ * Supports ${VAR} and ${VAR:-default} in string values.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.interpolateString = interpolateString;
41
+ exports.interpolateConfig = interpolateConfig;
42
+ exports.loadEnvFile = loadEnvFile;
43
+ exports.buildLookup = buildLookup;
44
+ const fs = __importStar(require("node:fs"));
45
+ const dotenv = __importStar(require("dotenv"));
46
+ const PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
47
+ /**
48
+ * Resolve ${VAR} / ${VAR:-default} placeholders in a single string.
49
+ * Empty-string follows bash `:-` semantics: an empty value falls back to the
50
+ * default when one is given. A ${VAR} without a default that resolves to
51
+ * undefined throws, naming the variable and the field it came from.
52
+ */
53
+ function interpolateString(input, lookup, fieldPath) {
54
+ return input.replace(PLACEHOLDER, (_match, name, defaultVal) => {
55
+ const value = lookup(name);
56
+ const isEmpty = value === undefined || value === '';
57
+ if (defaultVal !== undefined) {
58
+ return isEmpty ? defaultVal : value;
59
+ }
60
+ if (value === undefined) {
61
+ throw new Error(`Config references undefined env variable: ${name} (referenced in ${fieldPath})`);
62
+ }
63
+ return value;
64
+ });
65
+ }
66
+ /**
67
+ * Recursively interpolate all string values in a parsed config object.
68
+ * Objects/arrays are walked; non-string scalars are returned unchanged.
69
+ * The field path (e.g. `defaultHeaders.x-sap-password`) is threaded through for
70
+ * error messages.
71
+ */
72
+ function interpolateConfig(value, lookup, path = '') {
73
+ if (typeof value === 'string') {
74
+ return interpolateString(value, lookup, path || '(root)');
75
+ }
76
+ if (Array.isArray(value)) {
77
+ return value.map((item, i) => interpolateConfig(item, lookup, `${path}[${i}]`));
78
+ }
79
+ if (value !== null && typeof value === 'object') {
80
+ const out = {};
81
+ for (const [key, val] of Object.entries(value)) {
82
+ out[key] = interpolateConfig(val, lookup, path ? `${path}.${key}` : key);
83
+ }
84
+ return out;
85
+ }
86
+ return value;
87
+ }
88
+ /**
89
+ * Parse a .env file into a flat map. Throws if the path is given but missing —
90
+ * a specified-yet-absent secret source is a configuration error, not a no-op.
91
+ */
92
+ function loadEnvFile(envFilePath) {
93
+ if (!fs.existsSync(envFilePath)) {
94
+ throw new Error(`env file not found: ${envFilePath}`);
95
+ }
96
+ return dotenv.parse(fs.readFileSync(envFilePath, 'utf-8'));
97
+ }
98
+ /**
99
+ * Build a lookup over process.env (highest priority) then the parsed .env map.
100
+ */
101
+ function buildLookup(envFileMap) {
102
+ return (key) => process.env[key] ?? envFileMap[key];
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/proxy",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "MCP proxy server for SAP ABAP ADT - proxies local requests to cloud-llm-hub with JWT authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -71,6 +71,7 @@
71
71
  "@mcp-abap-adt/logger": "^0.1.4",
72
72
  "@modelcontextprotocol/sdk": "^1.26.0",
73
73
  "axios": "^1.15.1",
74
+ "dotenv": "^17.4.2",
74
75
  "js-yaml": "^4.1.1",
75
76
  "zod": "^4.3.6"
76
77
  },