@mcp-abap-adt/proxy 1.1.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.1] - 2026-05-26
11
+
12
+ ### Security
13
+ - **Default bind host changed from `0.0.0.0` to `127.0.0.1`** (loopback) for HTTP and SSE transports. The proxy holds auth tokens and runs locally, so it no longer listens on all network interfaces by default. Set `httpHost`/`sseHost` (or `MCP_HTTP_HOST`/`MCP_SSE_HOST`) to `0.0.0.0` to expose it intentionally.
14
+
15
+ ### Removed
16
+ - **Interactive configuration wizard (TUI)** — removed the `mcp-abap-adt-proxy tui` subcommand and the `src/tui/` module (its `direct`/`mcpUrl` scenario no longer matched the BTP-only proxy and produced unusable configs). Client and proxy configuration is handled by the separate `@mcp-abap-adt/configurator` package (`mcp-conf`). Dropped the `@inquirer/prompts` dependency.
17
+ - **Combined-launch test harness** — removed `tools/start-servers.js` and the `npm run test:servers` script. It started a local `mcp-abap-adt` server with the proxy in front of it via the long-removed `--mcp-url`/`--mcp` flags, which no longer applies to a pure authorization proxy.
18
+
19
+ ### Changed
20
+ - **Documentation synced to the BTP-only model.** Removed stale references to the long-gone `x-mcp-url`/`--mcp-url`, `mcpDestination`, `cloudLlmHubUrl`/`CLOUD_LLM_HUB_URL`, `DEBUG_HTTP_REQUESTS`, and config-file auto-discovery/`MCP_PROXY_CONFIG`. Documented the actual options: `defaultHeaders`, `browser`/`browserAuthPort`, and `targetUrl`/`x-target-url`. Removed the obsolete `IMPLEMENTATION_ANALYSIS.md`.
21
+
22
+ ## [1.2.0] - 2026-04-20
23
+
24
+ ### Added
25
+ - **Proactive JWT token refresh** — proxy now decodes the `exp` claim from the JWT and refreshes the token in the background 5 minutes before expiry via `authBroker.refreshToken()` (OAuth2 refresh_token grant, preserves role assignment). Eliminates latency spikes from on-demand 401 refreshes while keeping the reactive 401/403 fallback.
26
+
27
+ ### Changed
28
+ - **@mcp-abap-adt/interfaces 7.0** — updated from v6.0 to v7.0
29
+ - **typescript 6.0.3**, **@biomejs/biome 2.4.12**, **@modelcontextprotocol/inspector 0.21.2**, **axios 1.15.1**, **@inquirer/prompts 8.4.2** — patch bumps
30
+
10
31
  ## [1.1.0] - 2026-04-12
11
32
 
12
33
  ### Changed
package/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # @mcp-abap-adt/proxy
2
+ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
2
3
 
3
4
  MCP proxy server for SAP ABAP ADT - proxies local requests to MCP servers with JWT authentication.
4
5
 
@@ -141,7 +142,6 @@ The proxy uses BTP/XSUAA authentication:
141
142
  - **[Architecture](./docs/ARCHITECTURE.md)** - System architecture and design
142
143
  - **[Troubleshooting](./docs/TROUBLESHOOTING.md)** - Common issues and solutions
143
144
  - **[Routing Logic Specification](./docs/ROUTING_LOGIC.md)** - Detailed routing logic and scenarios
144
- - **[Implementation Analysis](./docs/IMPLEMENTATION_ANALYSIS.md)** - Current implementation analysis
145
145
  - **[Roadmap](./ROADMAP.md)** - Development roadmap and progress
146
146
 
147
147
  ## How It Works
@@ -225,28 +225,13 @@ See [Configuration Guide](./docs/CONFIGURATION.md) for complete options.
225
225
 
226
226
  ## Testing Tools
227
227
 
228
- ### Start Both Servers for Testing
229
-
230
- Use the included script to start both `mcp-abap-adt` and `mcp-abap-adt-proxy` simultaneously:
228
+ Verify a BTP destination's service key and token retrieval:
231
229
 
232
230
  ```bash
233
- # Using npm script
234
- npm run test:servers
235
-
236
- # Direct execution
237
- node tools/start-servers.js
238
-
239
- # With SSE transport
240
- node tools/start-servers.js --transport=sse
231
+ npm run test-destination
241
232
  ```
242
233
 
243
- The script automatically:
244
- - Starts ADT server on port 3000 (HTTP) or 3001 (SSE)
245
- - Starts Proxy server on port 3001 (HTTP) or 3002 (SSE)
246
- - Generates `mcpUrl` based on ADT server configuration
247
- - Ensures both servers use the same transport protocol
248
-
249
- See [tools/README.md](./tools/README.md) for complete documentation.
234
+ See [tools/README.md](./tools/README.md) for the available scripts.
250
235
 
251
236
  ## Development Status
252
237
 
@@ -257,7 +242,7 @@ See [tools/README.md](./tools/README.md) for complete documentation.
257
242
  - ✅ JWT Token Management & Proxy Forwarding
258
243
  - ✅ Configuration & Environment
259
244
  - ✅ Error Handling & Resilience
260
- - ✅ Testing Tools (`tools/start-servers.js`)
245
+ - ✅ Testing Tools (`tools/`)
261
246
  - ✅ Documentation
262
247
 
263
248
  🚧 **Future Work**
@@ -7,7 +7,6 @@
7
7
  *
8
8
  * Usage:
9
9
  * mcp-abap-adt-proxy [options]
10
- * mcp-abap-adt-proxy tui
11
10
  * mcp-abap-adt-proxy --transport=stdio
12
11
  * mcp-abap-adt-proxy --transport=http
13
12
  */
@@ -44,17 +43,13 @@ MCP ABAP ADT Proxy Server v${pkg.version}
44
43
 
45
44
  Usage:
46
45
  mcp-abap-adt-proxy [options]
47
- mcp-abap-adt-proxy tui
48
-
49
- Commands:
50
- tui Interactive configuration wizard (generates YAML config)
51
46
 
52
47
  Options:
53
48
  --transport=<type> Transport type: stdio, http, streamable-http, sse
54
49
  --http-port=<port> HTTP server port (default: 3001)
55
50
  --sse-port=<port> SSE server port (default: 3002)
56
- --http-host=<host> HTTP server host (default: 0.0.0.0)
57
- --sse-host=<host> SSE server host (default: 0.0.0.0)
51
+ --http-host=<host> HTTP server host (default: 127.0.0.1)
52
+ --sse-host=<host> SSE server host (default: 127.0.0.1)
58
53
  --btp=<destination> Override x-sap-destination header (for BTP Cloud authorization)
59
54
  Required for stdio/SSE transports unless provided in headers
60
55
  --target-url=<url> Override target URL (separates Auth from Connection)
@@ -70,8 +65,8 @@ Options:
70
65
  Environment Variables:
71
66
  MCP_HTTP_PORT HTTP server port (default: 3001)
72
67
  MCP_SSE_PORT SSE server port (default: 3002)
73
- MCP_HTTP_HOST HTTP server host (default: 0.0.0.0)
74
- MCP_SSE_HOST SSE server host (default: 0.0.0.0)
68
+ MCP_HTTP_HOST HTTP server host (default: 127.0.0.1)
69
+ MCP_SSE_HOST SSE server host (default: 127.0.0.1)
75
70
  MCP_TRANSPORT Transport type (stdio|http|sse)
76
71
  MCP_PROXY_UNSAFE Enable file-based session storage (set to "true")
77
72
 
@@ -85,7 +80,6 @@ Examples:
85
80
  mcp-abap-adt-proxy --btp=ai # With BTP destination override
86
81
  mcp-abap-adt-proxy --btp=ai --unsafe # With file-based session storage
87
82
  mcp-abap-adt-proxy --btp=ai --header x-sap-destination=S4HANA # With default SAP headers
88
- mcp-abap-adt-proxy tui # Interactive configuration wizard
89
83
  mcp-abap-adt-proxy --config=proxy-config.yaml # Load configuration from YAML file
90
84
  mcp-abap-adt-proxy -c proxy-config.yml # Load configuration from YAML file (short form)
91
85
 
@@ -146,30 +140,6 @@ function main() {
146
140
  process.exit(0);
147
141
  }
148
142
 
149
- // Handle 'tui' subcommand — interactive configuration wizard
150
- if (process.argv[2] === 'tui') {
151
- const wizardPath = path.resolve(__dirname, '../dist/tui/index.js');
152
- if (!fs.existsSync(wizardPath)) {
153
- process.stderr.write(`[MCP Proxy] ✗ Wizard not found at: ${wizardPath}\n`);
154
- process.stderr.write(`[MCP Proxy] Make sure to build the project with 'npm run build' first.\n`);
155
- process.exit(1);
156
- return;
157
- }
158
- import(wizardPath).then(({ runWizard }) => {
159
- runWizard().catch((err) => {
160
- if (err.name === 'ExitPromptError') {
161
- process.exit(0);
162
- }
163
- process.stderr.write(`[MCP Proxy] ✗ Wizard error: ${err.message}\n`);
164
- process.exit(1);
165
- });
166
- }).catch((err) => {
167
- process.stderr.write(`[MCP Proxy] ✗ Failed to load wizard: ${err.message}\n`);
168
- process.exit(1);
169
- });
170
- return;
171
- }
172
-
173
143
  // Spawn the main server process
174
144
  const serverPath = path.resolve(__dirname, '../dist/index.js');
175
145
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAuBH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAqBlC;;GAEG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,CAAW;gBAEhB,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAoDlE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAuE1B;;OAEG;YACW,eAAe;IAgG7B;;OAEG;YACW,kBAAkB;IA8ChC;;OAEG;YACW,cAAc;IA6T5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAmBhC;AAGD,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAuBH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAqBlC;;GAEG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,CAAW;gBAEhB,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAoDlE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAuE1B;;OAEG;YACW,eAAe;IAgG7B;;OAEG;YACW,kBAAkB;IA8ChC;;OAEG;YACW,cAAc;IA6T5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBhC;AAGD,eAAe,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -206,7 +206,7 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
206
206
  }
207
207
  });
208
208
  const port = httpConfig.port || 3001;
209
- const host = httpConfig.host || '0.0.0.0';
209
+ const host = httpConfig.host || '127.0.0.1';
210
210
  return new Promise((resolve, reject) => {
211
211
  this.httpServer?.listen(port, host, () => {
212
212
  const authSourceObj = this.config.btpDestination
@@ -513,7 +513,7 @@ Authorization source: ${authSourceObj}`;
513
513
  res.end('Not found');
514
514
  });
515
515
  const port = sseConfig.port || 3002;
516
- const host = sseConfig.host || '0.0.0.0';
516
+ const host = sseConfig.host || '127.0.0.1';
517
517
  return new Promise((resolve, reject) => {
518
518
  httpServer.listen(port, host, () => {
519
519
  const authSourceObj = this.config.btpDestination
@@ -544,6 +544,7 @@ Authorization source: ${authSourceObj}`;
544
544
  * Shutdown server
545
545
  */
546
546
  async shutdown() {
547
+ this.btpProxy?.dispose();
547
548
  try {
548
549
  await this.server.close();
549
550
  }
@@ -132,8 +132,8 @@ function applyDefaults(fileConfig) {
132
132
  const result = {
133
133
  httpPort: fileConfig.httpPort ?? 3001,
134
134
  ssePort: fileConfig.ssePort ?? 3002,
135
- httpHost: fileConfig.httpHost || '0.0.0.0',
136
- sseHost: fileConfig.sseHost || '0.0.0.0',
135
+ httpHost: fileConfig.httpHost || '127.0.0.1',
136
+ sseHost: fileConfig.sseHost || '127.0.0.1',
137
137
  logLevel: fileConfig.logLevel || 'info',
138
138
  btpDestination: fileConfig.btpDestination,
139
139
  targetUrl: fileConfig.targetUrl,
@@ -179,8 +179,8 @@ function loadFromEnv() {
179
179
  return {
180
180
  httpPort: parseInt(process.env.MCP_HTTP_PORT || '3001', 10),
181
181
  ssePort: parseInt(process.env.MCP_SSE_PORT || '3002', 10),
182
- httpHost: process.env.MCP_HTTP_HOST || '0.0.0.0',
183
- sseHost: process.env.MCP_SSE_HOST || '0.0.0.0',
182
+ httpHost: process.env.MCP_HTTP_HOST || '127.0.0.1',
183
+ sseHost: process.env.MCP_SSE_HOST || '127.0.0.1',
184
184
  logLevel: process.env.LOG_LEVEL || 'info',
185
185
  btpDestination,
186
186
  targetUrl,
@@ -64,7 +64,7 @@ function parseTransportConfig(fileOverrides) {
64
64
  const host = getArgValue('--sse-host') ||
65
65
  fileOverrides?.sseHost ||
66
66
  process.env.MCP_SSE_HOST ||
67
- '0.0.0.0';
67
+ '127.0.0.1';
68
68
  return {
69
69
  type: 'sse',
70
70
  host,
@@ -85,7 +85,7 @@ function parseTransportConfig(fileOverrides) {
85
85
  const host = getArgValue('--http-host') ||
86
86
  fileOverrides?.httpHost ||
87
87
  process.env.MCP_HTTP_HOST ||
88
- '0.0.0.0';
88
+ '127.0.0.1';
89
89
  return {
90
90
  type: 'streamable-http',
91
91
  host,
@@ -38,7 +38,9 @@ export declare class BtpProxy {
38
38
  private defaultBtpAuthBroker;
39
39
  private btpAuthBrokers;
40
40
  private tokenCache;
41
+ private refreshTimers;
41
42
  private readonly TOKEN_CACHE_TTL;
43
+ private readonly REFRESH_LEAD_MS;
42
44
  private circuitBreaker;
43
45
  private config;
44
46
  private unsafe;
@@ -64,6 +66,27 @@ export declare class BtpProxy {
64
66
  * @param forceRefresh Force token refresh
65
67
  */
66
68
  getJwtToken(destination: string, forceRefresh?: boolean): Promise<string>;
69
+ /**
70
+ * Decode JWT exp claim (seconds since epoch). Returns null if not parseable.
71
+ */
72
+ private decodeJwtExp;
73
+ /**
74
+ * Cache token with expiry derived from JWT, and schedule proactive refresh
75
+ * 5 minutes before expiry.
76
+ */
77
+ private cacheToken;
78
+ /**
79
+ * Schedule background refresh REFRESH_LEAD_MS before token expiry.
80
+ */
81
+ private scheduleProactiveRefresh;
82
+ /**
83
+ * Background refresh via auth-broker's refreshToken grant.
84
+ */
85
+ private proactiveRefresh;
86
+ /**
87
+ * Cancel all pending refresh timers. Call on shutdown.
88
+ */
89
+ dispose(): void;
67
90
  /**
68
91
  * Get the target service URL for a destination.
69
92
  * Priority: config.targetUrl > service key's serviceUrl
@@ -1 +1 @@
1
- {"version":3,"file":"btpProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/btpProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAgB,MAAM,2BAA2B,CAAC;AAYrE,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AA4CnE;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAU3C;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAGD,MAAM,MAAM,YAAY,GAAG,mBAAmB,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,UAAU,CACN;IACZ,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAU;gBAEZ,oBAAoB,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAqF3E;;;OAGG;IACU,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB3D;;;;OAIG;YACW,wBAAwB;IAqFtC;;OAEG;YACW,uBAAuB;IAyFrC;;;;OAIG;IACG,WAAW,CACf,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,MAAM,CAAC;IAqHlB;;;OAGG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAexD;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;;;;;;;;;;;;;;;OAgBG;YACW,iBAAiB;IAmT/B;;OAEG;IACG,YAAY,CAChB,eAAe,EAAE,YAAY,EAC7B,eAAe,EAAE,eAAe,EAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAC7D,OAAO,CAAC,aAAa,CAAC;IAiLzB;;OAEG;WACiB,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;CAyB7E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAEnB"}
1
+ {"version":3,"file":"btpProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/btpProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAgB,MAAM,2BAA2B,CAAC;AAYrE,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AA4CnE;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAU3C;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAGD,MAAM,MAAM,YAAY,GAAG,mBAAmB,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,UAAU,CACN;IACZ,OAAO,CAAC,aAAa,CAA0C;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAU;gBAEZ,oBAAoB,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAqF3E;;;OAGG;IACU,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB3D;;;;OAIG;YACW,wBAAwB;IAqFtC;;OAEG;YACW,uBAAuB;IAyFrC;;;;OAIG;IACG,WAAW,CACf,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,MAAM,CAAC;IAiHlB;;OAEG;IACH,OAAO,CAAC,YAAY;IAapB;;;OAGG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA6BhC;;OAEG;YACW,gBAAgB;IAU9B;;OAEG;IACI,OAAO,IAAI,IAAI;IAOtB;;;OAGG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAexD;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;;;;;;;;;;;;;;;OAgBG;YACW,iBAAiB;IAmT/B;;OAEG;IACG,YAAY,CAChB,eAAe,EAAE,YAAY,EAC7B,eAAe,EAAE,eAAe,EAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAC7D,OAAO,CAAC,aAAa,CAAC;IAiLzB;;OAEG;WACiB,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;CAyB7E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAEnB"}
@@ -58,7 +58,9 @@ class BtpProxy {
58
58
  defaultBtpAuthBroker;
59
59
  btpAuthBrokers = new Map();
60
60
  tokenCache = new Map();
61
- TOKEN_CACHE_TTL = 30 * 60 * 1000; // 30 minutes
61
+ refreshTimers = new Map();
62
+ TOKEN_CACHE_TTL = 30 * 60 * 1000; // fallback if JWT has no exp
63
+ REFRESH_LEAD_MS = 5 * 60 * 1000; // refresh 5 min before expiry
62
64
  circuitBreaker;
63
65
  config;
64
66
  unsafe;
@@ -349,11 +351,7 @@ class BtpProxy {
349
351
  });
350
352
  // Get token from auth-broker
351
353
  const token = await authBroker.getToken(destination);
352
- // Cache token (assume it's valid for 30 minutes)
353
- this.tokenCache.set(destination, {
354
- token,
355
- expiresAt: Date.now() + this.TOKEN_CACHE_TTL,
356
- });
354
+ this.cacheToken(destination, token);
357
355
  logger_js_1.logger?.debug('Retrieved JWT token from auth-broker', {
358
356
  type: 'JWT_TOKEN_RETRIEVED',
359
357
  destination,
@@ -404,6 +402,79 @@ class BtpProxy {
404
402
  throw new Error(errorMessage);
405
403
  }
406
404
  }
405
+ /**
406
+ * Decode JWT exp claim (seconds since epoch). Returns null if not parseable.
407
+ */
408
+ decodeJwtExp(token) {
409
+ const parts = token.split('.');
410
+ if (parts.length < 2)
411
+ return null;
412
+ try {
413
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
414
+ return typeof payload.exp === 'number' ? payload.exp : null;
415
+ }
416
+ catch {
417
+ return null;
418
+ }
419
+ }
420
+ /**
421
+ * Cache token with expiry derived from JWT, and schedule proactive refresh
422
+ * 5 minutes before expiry.
423
+ */
424
+ cacheToken(destination, token) {
425
+ const exp = this.decodeJwtExp(token);
426
+ const expiresAt = exp !== null ? exp * 1000 : Date.now() + this.TOKEN_CACHE_TTL;
427
+ this.tokenCache.set(destination, { token, expiresAt });
428
+ this.scheduleProactiveRefresh(destination, expiresAt);
429
+ }
430
+ /**
431
+ * Schedule background refresh REFRESH_LEAD_MS before token expiry.
432
+ */
433
+ scheduleProactiveRefresh(destination, expiresAt) {
434
+ const existing = this.refreshTimers.get(destination);
435
+ if (existing)
436
+ clearTimeout(existing);
437
+ const delay = expiresAt - Date.now() - this.REFRESH_LEAD_MS;
438
+ if (delay <= 0) {
439
+ // Token already within lead window or expired; skip scheduling.
440
+ this.refreshTimers.delete(destination);
441
+ return;
442
+ }
443
+ const timer = setTimeout(() => {
444
+ this.refreshTimers.delete(destination);
445
+ this.proactiveRefresh(destination).catch((err) => {
446
+ logger_js_1.logger?.warn('Proactive token refresh failed, will retry reactively', {
447
+ type: 'JWT_TOKEN_PROACTIVE_REFRESH_ERROR',
448
+ destination,
449
+ error: err instanceof Error ? err.message : String(err),
450
+ });
451
+ });
452
+ }, delay);
453
+ // Don't keep the event loop alive just for refresh timers.
454
+ timer.unref?.();
455
+ this.refreshTimers.set(destination, timer);
456
+ }
457
+ /**
458
+ * Background refresh via auth-broker's refreshToken grant.
459
+ */
460
+ async proactiveRefresh(destination) {
461
+ logger_js_1.logger?.info('Proactively refreshing JWT token', {
462
+ type: 'JWT_TOKEN_PROACTIVE_REFRESH',
463
+ destination,
464
+ });
465
+ const broker = await this.getOrCreateBtpAuthBroker(destination);
466
+ const token = await broker.refreshToken(destination);
467
+ this.cacheToken(destination, token);
468
+ }
469
+ /**
470
+ * Cancel all pending refresh timers. Call on shutdown.
471
+ */
472
+ dispose() {
473
+ for (const timer of this.refreshTimers.values()) {
474
+ clearTimeout(timer);
475
+ }
476
+ this.refreshTimers.clear();
477
+ }
407
478
  /**
408
479
  * Get the target service URL for a destination.
409
480
  * Priority: config.targetUrl > service key's serviceUrl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/proxy",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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",
@@ -19,7 +19,6 @@
19
19
  "dev": "npx @modelcontextprotocol/inspector node ./dist/index.js",
20
20
  "clean": "rimraf dist tsconfig.tsbuildinfo",
21
21
  "prepublishOnly": "npm run build",
22
- "test:servers": "node ./tools/start-servers.js",
23
22
  "test-destination": "npx tsx tools/test-destination.ts"
24
23
  },
25
24
  "keywords": [
@@ -44,9 +43,9 @@
44
43
  "access": "public"
45
44
  },
46
45
  "devDependencies": {
47
- "@biomejs/biome": "^2.3.15",
46
+ "@biomejs/biome": "^2.4.12",
48
47
  "@jest/globals": "^30.2.0",
49
- "@modelcontextprotocol/inspector": "^0.21.1",
48
+ "@modelcontextprotocol/inspector": "^0.21.2",
50
49
  "@types/jest": "^30.0.0",
51
50
  "@types/js-yaml": "^4.0.9",
52
51
  "@types/node": "^25.2.3",
@@ -61,18 +60,17 @@
61
60
  "rimraf": "^6.1.2",
62
61
  "ts-jest": "^29.2.5",
63
62
  "tsx": "^4.21.0",
64
- "typescript": "^6.0.2"
63
+ "typescript": "^6.0.3"
65
64
  },
66
65
  "dependencies": {
67
- "@inquirer/prompts": "^8.3.2",
68
66
  "@mcp-abap-adt/auth-broker": "^1.0.5",
69
67
  "@mcp-abap-adt/auth-providers": "^1.0.5",
70
68
  "@mcp-abap-adt/auth-stores": "^1.0.4",
71
69
  "@mcp-abap-adt/header-validator": "^0.1.8",
72
- "@mcp-abap-adt/interfaces": "^6.0.0",
70
+ "@mcp-abap-adt/interfaces": "^7.0.0",
73
71
  "@mcp-abap-adt/logger": "^0.1.4",
74
72
  "@modelcontextprotocol/sdk": "^1.26.0",
75
- "axios": "^1.13.5",
73
+ "axios": "^1.15.1",
76
74
  "js-yaml": "^4.1.1",
77
75
  "zod": "^4.3.6"
78
76
  },
@@ -1,8 +0,0 @@
1
- /**
2
- * TUI module barrel export
3
- */
4
- export type { ServiceKeyCheckResult } from './serviceKeyCheck.js';
5
- export { checkServiceKeyExists } from './serviceKeyCheck.js';
6
- export type { WizardAnswers } from './wizard.js';
7
- export { generateConfigYaml, runWizard } from './wizard.js';
8
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tui/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
package/dist/tui/index.js DELETED
@@ -1,11 +0,0 @@
1
- "use strict";
2
- /**
3
- * TUI module barrel export
4
- */
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.runWizard = exports.generateConfigYaml = exports.checkServiceKeyExists = void 0;
7
- var serviceKeyCheck_js_1 = require("./serviceKeyCheck.js");
8
- Object.defineProperty(exports, "checkServiceKeyExists", { enumerable: true, get: function () { return serviceKeyCheck_js_1.checkServiceKeyExists; } });
9
- var wizard_js_1 = require("./wizard.js");
10
- Object.defineProperty(exports, "generateConfigYaml", { enumerable: true, get: function () { return wizard_js_1.generateConfigYaml; } });
11
- Object.defineProperty(exports, "runWizard", { enumerable: true, get: function () { return wizard_js_1.runWizard; } });
@@ -1,25 +0,0 @@
1
- /**
2
- * Service key existence checker
3
- * Checks if a service key file exists for a given BTP destination
4
- * by searching platform-specific directories.
5
- */
6
- /**
7
- * Result of a service key existence check
8
- */
9
- export interface ServiceKeyCheckResult {
10
- /** Whether the service key file was found */
11
- found: boolean;
12
- /** Absolute path to the found service key file (if found) */
13
- path?: string;
14
- /** Directories that were searched (when not found) */
15
- searchedPaths?: string[];
16
- }
17
- /**
18
- * Check if a service key file exists for the given BTP destination.
19
- * Searches all platform-specific directories returned by getPlatformPaths('service-keys').
20
- *
21
- * @param destination - BTP destination name (without .json extension)
22
- * @returns ServiceKeyCheckResult indicating whether the key was found and where
23
- */
24
- export declare function checkServiceKeyExists(destination: string): ServiceKeyCheckResult;
25
- //# sourceMappingURL=serviceKeyCheck.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"serviceKeyCheck.d.ts","sourceRoot":"","sources":["../../src/tui/serviceKeyCheck.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,6CAA6C;IAC7C,KAAK,EAAE,OAAO,CAAC;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,GAClB,qBAAqB,CAYvB"}
@@ -1,62 +0,0 @@
1
- "use strict";
2
- /**
3
- * Service key existence checker
4
- * Checks if a service key file exists for a given BTP destination
5
- * by searching platform-specific directories.
6
- */
7
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
- if (k2 === undefined) k2 = k;
9
- var desc = Object.getOwnPropertyDescriptor(m, k);
10
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
- desc = { enumerable: true, get: function() { return m[k]; } };
12
- }
13
- Object.defineProperty(o, k2, desc);
14
- }) : (function(o, m, k, k2) {
15
- if (k2 === undefined) k2 = k;
16
- o[k2] = m[k];
17
- }));
18
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
- Object.defineProperty(o, "default", { enumerable: true, value: v });
20
- }) : function(o, v) {
21
- o["default"] = v;
22
- });
23
- var __importStar = (this && this.__importStar) || (function () {
24
- var ownKeys = function(o) {
25
- ownKeys = Object.getOwnPropertyNames || function (o) {
26
- var ar = [];
27
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
- return ar;
29
- };
30
- return ownKeys(o);
31
- };
32
- return function (mod) {
33
- if (mod && mod.__esModule) return mod;
34
- var result = {};
35
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
- __setModuleDefault(result, mod);
37
- return result;
38
- };
39
- })();
40
- Object.defineProperty(exports, "__esModule", { value: true });
41
- exports.checkServiceKeyExists = checkServiceKeyExists;
42
- const fs = __importStar(require("node:fs"));
43
- const path = __importStar(require("node:path"));
44
- const stores_js_1 = require("../lib/stores.js");
45
- /**
46
- * Check if a service key file exists for the given BTP destination.
47
- * Searches all platform-specific directories returned by getPlatformPaths('service-keys').
48
- *
49
- * @param destination - BTP destination name (without .json extension)
50
- * @returns ServiceKeyCheckResult indicating whether the key was found and where
51
- */
52
- function checkServiceKeyExists(destination) {
53
- const searchDirs = (0, stores_js_1.getPlatformPaths)('service-keys');
54
- const fileName = `${destination}.json`;
55
- for (const dir of searchDirs) {
56
- const filePath = path.join(dir, fileName);
57
- if (fs.existsSync(filePath)) {
58
- return { found: true, path: filePath };
59
- }
60
- }
61
- return { found: false, searchedPaths: searchDirs };
62
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * TUI wizard for interactive proxy configuration
3
- */
4
- /**
5
- * Answers collected by the wizard
6
- */
7
- export interface WizardAnswers {
8
- scenario: 'btp' | 'direct';
9
- btpDestination?: string;
10
- targetUrl?: string;
11
- mcpDestination?: string;
12
- mcpUrl?: string;
13
- transport: 'stdio' | 'http' | 'sse';
14
- httpHost?: string;
15
- httpPort?: number;
16
- sseHost?: string;
17
- ssePort?: number;
18
- browser: 'system' | 'headless' | 'chrome' | 'edge' | 'firefox' | 'none';
19
- browserAuthPort: number;
20
- unsafe: boolean;
21
- logLevel: string;
22
- maxRetries?: number;
23
- retryDelay?: number;
24
- requestTimeout?: number;
25
- circuitBreakerThreshold?: number;
26
- circuitBreakerTimeout?: number;
27
- cloudLlmHubUrl?: string;
28
- }
29
- /**
30
- * Generate a YAML configuration string from wizard answers.
31
- * The output follows the structure of docs/mcp-proxy-config.example.yaml.
32
- */
33
- export declare function generateConfigYaml(answers: WizardAnswers): string;
34
- /**
35
- * Run the interactive configuration wizard
36
- */
37
- export declare function runWizard(): Promise<WizardAnswers>;
38
- //# sourceMappingURL=wizard.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"wizard.d.ts","sourceRoot":"","sources":["../../src/tui/wizard.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,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;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAWD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CA+FjE;AAeD;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,aAAa,CAAC,CAwMxD"}
@@ -1,321 +0,0 @@
1
- "use strict";
2
- /**
3
- * TUI wizard for interactive proxy configuration
4
- */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
- Object.defineProperty(o, "default", { enumerable: true, value: v });
18
- }) : function(o, v) {
19
- o["default"] = v;
20
- });
21
- var __importStar = (this && this.__importStar) || (function () {
22
- var ownKeys = function(o) {
23
- ownKeys = Object.getOwnPropertyNames || function (o) {
24
- var ar = [];
25
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
- return ar;
27
- };
28
- return ownKeys(o);
29
- };
30
- return function (mod) {
31
- if (mod && mod.__esModule) return mod;
32
- var result = {};
33
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
- __setModuleDefault(result, mod);
35
- return result;
36
- };
37
- })();
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.generateConfigYaml = generateConfigYaml;
40
- exports.runWizard = runWizard;
41
- const fs = __importStar(require("node:fs"));
42
- const serviceKeyCheck_js_1 = require("./serviceKeyCheck.js");
43
- /** Default values for advanced settings */
44
- const DEFAULTS = {
45
- maxRetries: 3,
46
- retryDelay: 1000,
47
- requestTimeout: 60000,
48
- circuitBreakerThreshold: 5,
49
- circuitBreakerTimeout: 60000,
50
- };
51
- /**
52
- * Generate a YAML configuration string from wizard answers.
53
- * The output follows the structure of docs/mcp-proxy-config.example.yaml.
54
- */
55
- function generateConfigYaml(answers) {
56
- const lines = [];
57
- lines.push('# MCP ABAP ADT Proxy Configuration');
58
- lines.push('');
59
- // Transport configuration
60
- lines.push('# Transport configuration');
61
- lines.push(`transport: ${answers.transport} # stdio | http | sse`);
62
- if (answers.transport === 'http' || answers.transport === 'sse') {
63
- lines.push(`httpPort: ${answers.httpPort ?? 3001}`);
64
- lines.push(`httpHost: "${answers.httpHost ?? '0.0.0.0'}"`);
65
- }
66
- if (answers.transport === 'sse') {
67
- lines.push(`ssePort: ${answers.ssePort ?? 3002}`);
68
- lines.push(`sseHost: "${answers.sseHost ?? '0.0.0.0'}"`);
69
- }
70
- lines.push('');
71
- // Destination overrides
72
- lines.push('# Destination overrides');
73
- if (answers.scenario === 'btp') {
74
- if (answers.btpDestination) {
75
- lines.push(`btpDestination: "${answers.btpDestination}"`);
76
- }
77
- if (answers.targetUrl) {
78
- lines.push(`targetUrl: "${answers.targetUrl}"`);
79
- }
80
- if (answers.mcpDestination) {
81
- lines.push(`mcpDestination: "${answers.mcpDestination}"`);
82
- }
83
- if (answers.mcpUrl) {
84
- lines.push(`# mcpUrl: "${answers.mcpUrl}"`);
85
- }
86
- else {
87
- lines.push('# mcpUrl:');
88
- }
89
- }
90
- else {
91
- lines.push('# btpDestination:');
92
- lines.push('# mcpDestination:');
93
- if (answers.mcpUrl) {
94
- lines.push(`mcpUrl: "${answers.mcpUrl}"`);
95
- }
96
- }
97
- lines.push('');
98
- // Browser authentication
99
- lines.push('# Browser authentication');
100
- lines.push(`browser: "${answers.browser}"`);
101
- lines.push(`browserAuthPort: ${answers.browserAuthPort}`);
102
- lines.push('');
103
- // Session storage mode
104
- lines.push('# Session storage mode');
105
- lines.push(`unsafe: ${answers.unsafe} # If true, persists tokens to disk`);
106
- lines.push('');
107
- // Error handling & resilience
108
- lines.push('# Error handling & resilience');
109
- lines.push(`maxRetries: ${answers.maxRetries ?? DEFAULTS.maxRetries}`);
110
- lines.push(`retryDelay: ${answers.retryDelay ?? DEFAULTS.retryDelay} # milliseconds`);
111
- lines.push(`requestTimeout: ${answers.requestTimeout ?? DEFAULTS.requestTimeout} # milliseconds`);
112
- lines.push(`circuitBreakerThreshold: ${answers.circuitBreakerThreshold ?? DEFAULTS.circuitBreakerThreshold}`);
113
- lines.push(`circuitBreakerTimeout: ${answers.circuitBreakerTimeout ?? DEFAULTS.circuitBreakerTimeout} # milliseconds`);
114
- lines.push('');
115
- // Logging
116
- lines.push('# Logging');
117
- lines.push(`logLevel: "${answers.logLevel}" # debug | info | warn | error`);
118
- // Cloud LLM Hub URL
119
- if (answers.cloudLlmHubUrl) {
120
- lines.push('');
121
- lines.push('# Cloud LLM Hub URL');
122
- lines.push(`cloudLlmHubUrl: "${answers.cloudLlmHubUrl}"`);
123
- }
124
- lines.push('');
125
- return lines.join('\n');
126
- }
127
- /**
128
- * Print a service key check result to the console.
129
- */
130
- function printServiceKeyCheck(destination) {
131
- const result = (0, serviceKeyCheck_js_1.checkServiceKeyExists)(destination);
132
- if (result.found) {
133
- console.log(` Found: ${result.path}`);
134
- }
135
- else {
136
- console.log(` Warning: Service key file not found for "${destination}".`);
137
- console.log(` Searched: ${result.searchedPaths?.join(', ') ?? '(none)'}`);
138
- }
139
- }
140
- /**
141
- * Run the interactive configuration wizard
142
- */
143
- async function runWizard() {
144
- try {
145
- const { select, input, confirm, number } = await import('@inquirer/prompts');
146
- console.log('\n MCP ABAP ADT Proxy — Configuration Wizard\n');
147
- // 1. Scenario
148
- const scenario = await select({
149
- message: 'Select scenario:',
150
- choices: [
151
- {
152
- name: 'BTP service (XSUAA auth via service key)',
153
- value: 'btp',
154
- },
155
- {
156
- name: 'Direct MCP server URL (no auth)',
157
- value: 'direct',
158
- },
159
- ],
160
- });
161
- const answers = { scenario };
162
- // 2. Scenario-specific questions
163
- if (scenario === 'btp') {
164
- const btpDestination = await input({
165
- message: 'BTP destination name:',
166
- required: true,
167
- });
168
- answers.btpDestination = btpDestination;
169
- printServiceKeyCheck(btpDestination);
170
- const targetUrl = await input({
171
- message: 'Target service URL (leave empty if in service key):',
172
- default: '',
173
- });
174
- if (targetUrl) {
175
- answers.targetUrl = targetUrl;
176
- }
177
- const addMcpDest = await confirm({
178
- message: 'Add MCP destination (for ABAP connection params on Cloud)?',
179
- default: false,
180
- });
181
- if (addMcpDest) {
182
- const mcpDestination = await input({
183
- message: 'MCP destination name:',
184
- required: true,
185
- });
186
- answers.mcpDestination = mcpDestination;
187
- printServiceKeyCheck(mcpDestination);
188
- }
189
- }
190
- else {
191
- const mcpUrl = await input({
192
- message: 'MCP server URL:',
193
- required: true,
194
- });
195
- answers.mcpUrl = mcpUrl;
196
- }
197
- // 3. Transport
198
- const transport = await select({
199
- message: 'Select transport:',
200
- choices: [
201
- { name: 'stdio', value: 'stdio' },
202
- { name: 'streamable-http', value: 'http' },
203
- { name: 'sse', value: 'sse' },
204
- ],
205
- });
206
- answers.transport = transport;
207
- // 4. Transport-specific settings
208
- if (transport !== 'stdio') {
209
- answers.httpHost = await input({
210
- message: 'HTTP host:',
211
- default: '0.0.0.0',
212
- });
213
- answers.httpPort = await number({
214
- message: 'HTTP port:',
215
- default: 3001,
216
- min: 1,
217
- max: 65535,
218
- });
219
- if (transport === 'sse') {
220
- answers.sseHost = await input({
221
- message: 'SSE host:',
222
- default: '0.0.0.0',
223
- });
224
- answers.ssePort = await number({
225
- message: 'SSE port:',
226
- default: 3002,
227
- min: 1,
228
- max: 65535,
229
- });
230
- }
231
- }
232
- // 5. Browser
233
- answers.browser = await select({
234
- message: 'Select browser for OAuth2 login:',
235
- choices: [
236
- { name: 'system (default browser)', value: 'system' },
237
- { name: 'chrome', value: 'chrome' },
238
- { name: 'edge', value: 'edge' },
239
- { name: 'firefox', value: 'firefox' },
240
- { name: 'headless', value: 'headless' },
241
- { name: 'none (disable)', value: 'none' },
242
- ],
243
- });
244
- // 6. OAuth2 callback port
245
- answers.browserAuthPort = await number({
246
- message: 'OAuth2 callback port:',
247
- default: 3333,
248
- });
249
- // 7. Unsafe mode
250
- answers.unsafe = await confirm({
251
- message: 'Unsafe mode (persist tokens to disk)?',
252
- default: false,
253
- });
254
- // 8. Log level
255
- answers.logLevel = await select({
256
- message: 'Log level:',
257
- choices: [
258
- { name: 'debug', value: 'debug' },
259
- { name: 'info', value: 'info' },
260
- { name: 'warn', value: 'warn' },
261
- { name: 'error', value: 'error' },
262
- ],
263
- default: 'info',
264
- });
265
- // 9. Advanced settings
266
- const configureAdvanced = await confirm({
267
- message: 'Configure advanced settings?',
268
- default: false,
269
- });
270
- if (configureAdvanced) {
271
- answers.maxRetries = await number({
272
- message: 'Max retries:',
273
- default: DEFAULTS.maxRetries,
274
- });
275
- answers.retryDelay = await number({
276
- message: 'Retry delay (ms):',
277
- default: DEFAULTS.retryDelay,
278
- });
279
- answers.requestTimeout = await number({
280
- message: 'Request timeout (ms):',
281
- default: DEFAULTS.requestTimeout,
282
- });
283
- answers.circuitBreakerThreshold = await number({
284
- message: 'Circuit breaker threshold:',
285
- default: DEFAULTS.circuitBreakerThreshold,
286
- });
287
- answers.circuitBreakerTimeout = await number({
288
- message: 'Circuit breaker timeout (ms):',
289
- default: DEFAULTS.circuitBreakerTimeout,
290
- });
291
- const setCloudUrl = await confirm({
292
- message: 'Set Cloud LLM Hub URL?',
293
- default: false,
294
- });
295
- if (setCloudUrl) {
296
- answers.cloudLlmHubUrl = await input({
297
- message: 'Cloud LLM Hub URL:',
298
- required: true,
299
- });
300
- }
301
- }
302
- // 10. Output file
303
- const outputPath = await input({
304
- message: 'Output file path:',
305
- default: './mcp-proxy-config.yaml',
306
- });
307
- // Generate and write config
308
- const fullAnswers = answers;
309
- const yaml = generateConfigYaml(fullAnswers);
310
- fs.writeFileSync(outputPath, yaml, 'utf-8');
311
- console.log(`\nConfig saved to: ${outputPath}`);
312
- console.log(`Run with: mcp-abap-adt-proxy --config ${outputPath}`);
313
- return fullAnswers;
314
- }
315
- catch (error) {
316
- if (error instanceof Error && error.name === 'ExitPromptError') {
317
- process.exit(0);
318
- }
319
- throw error;
320
- }
321
- }