@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 +9 -0
- package/README.md +23 -1
- package/bin/mcp-abap-adt-proxy.js +4 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +28 -4
- package/dist/lib/envInterpolation.d.ts +28 -0
- package/dist/lib/envInterpolation.d.ts.map +1 -0
- package/dist/lib/envInterpolation.js +103 -0
- package/package.json +2 -1
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
|
-
|
|
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
|
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
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"}
|
package/dist/lib/config.js
CHANGED
|
@@ -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
|
|
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
|
|
223
|
-
defaultHeaders[key] =
|
|
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.
|
|
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
|
},
|