@mcp-abap-adt/proxy 1.2.1 → 1.5.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,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.5.0] - 2026-06-02
11
+
12
+ ### Added
13
+ - **Exit on incomplete authentication.** When authentication is requested but not completed — at startup (eager auth) or on a request that needs a token — the proxy now logs a categorised reason (timeout / invalid credentials / service key / network) and exits with code 1, so it can be restarted to retry (mirroring the one-shot `mcp-auth` command) instead of running in a permanently unauthenticated state. A downstream/forward error (after auth succeeds) is still returned to the client without exiting.
14
+
15
+ ### Changed
16
+ - Bump `@mcp-abap-adt/auth-broker` `^1.0.5` → `^1.0.6`: the broker no longer starts a redundant second browser login after an interactive session-login failure, so the real cause (e.g. login timeout) is reported instead of a misleading `Port <n> is already in use`.
17
+
18
+ ## [1.4.0] - 2026-06-02
19
+
20
+ ### Added
21
+ - **Manual paste login for `--browser none` / `headless`** (via `@mcp-abap-adt/auth-providers@1.1.0`). When the browser runs on a different machine than the proxy, login can be completed by opening the paste form at `http://<host>:<browser-auth-port>/` and pasting the `code` (or the whole redirected URL), or by pasting the code in the terminal — in addition to the existing automatic localhost callback.
22
+
23
+ ### Fixed
24
+ - **`--browser none` now prints the authorization URL.** The per-destination `AuthorizationCodeProvider` was created without a logger, so the "Open this URL" prompt from `browserAuth` was silently dropped and login could never be completed in headless mode. The proxy now passes its logger to the provider; `auth-providers@1.1.0` additionally falls back to `stderr` when no logger is supplied.
25
+ - **Platform-correct service-key path in `--help` and errors.** The "service key not found" hint and `--help` hard-coded the Unix `~/.config/...` path, which is wrong on Windows (`%USERPROFILE%\Documents\mcp-abap-adt\service-keys`). Both now show the real resolved directory and mention the `AUTH_BROKER_PATH` override.
26
+
27
+ ### Changed
28
+ - Bump `@mcp-abap-adt/auth-providers` `^1.0.5` → `^1.1.0`.
29
+
30
+ ## [1.3.0] - 2026-05-28
31
+
32
+ ### Added
33
+ - **CLI flags override values from `--config`** — when a config file is loaded via `--config`/`-c`, explicit CLI flags (`--btp`, `--target-url`/`--url`, `--unsafe`, `--browser`, `--browser-auth-port`, `--header`) now override matching values from the file instead of being silently dropped. `--header` entries merge with `defaultHeaders` (CLI keys win on conflict). The proxy logs which keys were overridden so the precedence is visible.
34
+ - **`--help` / `-h`** — full CLI usage printer covering auth, browser, transport flags and the documented environment variables. Previously the proxy had no help output at all, making the `--browser none` / `--browser-auth-port` flags effectively undiscoverable.
35
+ - **Graceful shutdown on `SIGINT` / `SIGTERM`** — the proxy now wires both signals to `shutdown()`, which closes the MCP server and HTTP listener, clears pending JWT refresh timers, drops the token cache, and releases per-destination auth brokers. Previously these resources were only released on hard process exit.
36
+
37
+ ### Fixed
38
+ - `BtpProxy.dispose()` now clears the token cache and the per-destination broker map in addition to refresh timers, so a clean shutdown actually releases all auth state.
39
+
10
40
  ## [1.2.1] - 2026-05-26
11
41
 
12
42
  ### Security
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export declare class McpAbapAdtProxyServer {
17
17
  private config;
18
18
  private httpServer?;
19
19
  private btpProxy?;
20
+ private authExiting;
20
21
  constructor(transportConfig?: TransportConfig, configPath?: string);
21
22
  /**
22
23
  * Setup MCP handlers
@@ -38,6 +39,13 @@ export declare class McpAbapAdtProxyServer {
38
39
  * Start SSE server
39
40
  */
40
41
  private startSseServer;
42
+ /**
43
+ * Authentication was requested but could not be completed. Report the reason
44
+ * clearly, release resources, and exit(1) so the proxy is restarted (and the
45
+ * user gets a fresh login prompt) instead of running unauthenticated. Mirrors
46
+ * the one-shot `mcp-auth` command, which exits on a failed/incomplete login.
47
+ */
48
+ private fatalAuthFailure;
41
49
  /**
42
50
  * Shutdown server
43
51
  */
@@ -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;CAoBhC;AAGD,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwBH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAgElC;;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;IAC5B,OAAO,CAAC,WAAW,CAAS;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;IA2E1B;;OAEG;YACW,eAAe;IAgG7B;;OAEG;YACW,kBAAkB;IAgEhC;;OAEG;YACW,cAAc;IA6T5B;;;;;OAKG;YACW,gBAAgB;IAiC9B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBhC;AAGD,eAAe,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
18
18
  const reverseProxy_js_1 = require("./proxy/reverseProxy.js");
19
19
  const config_js_1 = require("./lib/config.js");
20
20
  const logger_js_1 = require("./lib/logger.js");
21
+ const stores_js_1 = require("./lib/stores.js");
21
22
  const transportConfig_js_1 = require("./lib/transportConfig.js");
22
23
  const btpProxy_js_1 = require("./proxy/btpProxy.js");
23
24
  const headerAnalyzer_js_1 = require("./router/headerAnalyzer.js");
@@ -31,6 +32,38 @@ function getBodyId(body) {
31
32
  }
32
33
  return null;
33
34
  }
35
+ /**
36
+ * Classify an authentication failure into a human-readable reason so the proxy
37
+ * can report *why* it is exiting (timeout vs bad credentials vs network ...).
38
+ */
39
+ function classifyAuthFailure(error) {
40
+ const msg = error instanceof Error ? error.message : String(error);
41
+ if (/timeout/i.test(msg)) {
42
+ return {
43
+ category: 'timeout',
44
+ reason: 'the login URL was not completed in time',
45
+ };
46
+ }
47
+ if (/service key|missing required fields/i.test(msg)) {
48
+ return {
49
+ category: 'service-key',
50
+ reason: 'service key missing or incomplete',
51
+ };
52
+ }
53
+ if (/invalid_client|unauthorized|\b401\b|invalid credentials/i.test(msg)) {
54
+ return {
55
+ category: 'credentials',
56
+ reason: 'invalid credentials — UAA rejected the client',
57
+ };
58
+ }
59
+ if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network|ENETUNREACH/i.test(msg)) {
60
+ return {
61
+ category: 'network',
62
+ reason: 'network error reaching the authentication server',
63
+ };
64
+ }
65
+ return { category: 'other', reason: msg };
66
+ }
34
67
  /**
35
68
  * MCP ABAP ADT Proxy Server
36
69
  */
@@ -40,6 +73,7 @@ class McpAbapAdtProxyServer {
40
73
  config;
41
74
  httpServer;
42
75
  btpProxy;
76
+ authExiting = false;
43
77
  constructor(transportConfig, configPath) {
44
78
  // Load config first to extract transport overrides from YAML/JSON file
45
79
  const resolvedConfigPath = configPath || (0, config_js_1.getConfigPath)();
@@ -125,20 +159,24 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
125
159
  });
126
160
  return;
127
161
  }
128
- // Eager initialization of BTP authentication
162
+ // Eager initialization of BTP authentication. Kept in the background so the
163
+ // server starts listening immediately (and, for --browser none, the auth URL
164
+ // is printed right away). If auth is requested but never completed, exit(1)
165
+ // so the proxy is restarted and the user gets a fresh login prompt.
129
166
  if (this.config.btpDestination) {
167
+ const dest = this.config.btpDestination;
130
168
  try {
131
169
  if (!this.btpProxy) {
132
170
  this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
133
171
  }
134
- // Trigger initialization (background) - this will open the browser
135
- this.btpProxy.initialize(this.config.btpDestination);
136
172
  }
137
173
  catch (error) {
138
- logger_js_1.logger?.error('Failed to initialize BTP proxy for eager auth', {
139
- error: error instanceof Error ? error.message : String(error),
140
- });
174
+ await this.fatalAuthFailure(error, dest, 'startup');
175
+ return;
141
176
  }
177
+ this.btpProxy
178
+ .initialize(dest)
179
+ .catch((error) => this.fatalAuthFailure(error, dest, 'startup'));
142
180
  }
143
181
  if (this.transportConfig.type === 'streamable-http') {
144
182
  await this.startHttpServer();
@@ -246,9 +284,28 @@ Authorization source: ${authSourceObj}`;
246
284
  if (!this.btpProxy) {
247
285
  this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
248
286
  }
287
+ // Authentication is separated from forwarding: an auth failure is fatal
288
+ // (the proxy exits so it can be restarted), while a downstream/forward error
289
+ // is returned to the client without killing the proxy.
290
+ let jwtToken;
249
291
  try {
250
292
  // Get JWT token (cached, auto-refresh)
251
- const jwtToken = await this.btpProxy.getJwtToken(destination);
293
+ jwtToken = await this.btpProxy.getJwtToken(destination);
294
+ }
295
+ catch (authError) {
296
+ logger_js_1.logger?.error('Proxy request failed: authentication error', {
297
+ type: 'PROXY_REQUEST_AUTH_ERROR',
298
+ destination,
299
+ error: authError instanceof Error ? authError.message : String(authError),
300
+ });
301
+ if (!res.headersSent) {
302
+ res.writeHead(502, { 'Content-Type': 'application/json' });
303
+ res.end(JSON.stringify({ error: 'Authentication failed' }));
304
+ }
305
+ await this.fatalAuthFailure(authError, destination, 'request');
306
+ return;
307
+ }
308
+ try {
252
309
  // Get target URL
253
310
  const targetUrl = intercepted.routingDecision.targetUrl ||
254
311
  (await this.btpProxy.getTargetUrl(destination));
@@ -540,6 +597,37 @@ Authorization source: ${authSourceObj}`;
540
597
  });
541
598
  });
542
599
  }
600
+ /**
601
+ * Authentication was requested but could not be completed. Report the reason
602
+ * clearly, release resources, and exit(1) so the proxy is restarted (and the
603
+ * user gets a fresh login prompt) instead of running unauthenticated. Mirrors
604
+ * the one-shot `mcp-auth` command, which exits on a failed/incomplete login.
605
+ */
606
+ async fatalAuthFailure(error, destination, phase) {
607
+ if (this.authExiting)
608
+ return;
609
+ this.authExiting = true;
610
+ const { category, reason } = classifyAuthFailure(error);
611
+ const detail = error instanceof Error ? error.message : String(error);
612
+ logger_js_1.logger?.error('Authentication not completed — exiting', {
613
+ type: 'AUTH_FATAL_EXIT',
614
+ destination,
615
+ phase,
616
+ category,
617
+ reason,
618
+ error: detail,
619
+ });
620
+ console.error(`[MCP Proxy] ✗ Authentication failed for destination "${destination}" (${phase}): ${reason}.\n` +
621
+ `[MCP Proxy] ${detail}\n` +
622
+ `[MCP Proxy] Exiting (code 1) so the proxy can be restarted to retry authentication.`);
623
+ try {
624
+ await this.shutdown();
625
+ }
626
+ catch {
627
+ // best-effort cleanup; exit regardless
628
+ }
629
+ process.exit(1);
630
+ }
543
631
  /**
544
632
  * Shutdown server
545
633
  */
@@ -567,9 +655,98 @@ Authorization source: ${authSourceObj}`;
567
655
  exports.McpAbapAdtProxyServer = McpAbapAdtProxyServer;
568
656
  // Export for use in bin script
569
657
  exports.default = McpAbapAdtProxyServer;
658
+ /**
659
+ * Print CLI usage and exit.
660
+ */
661
+ function printHelp() {
662
+ // Show the real, platform-specific directory where service keys are read from
663
+ // (Windows: %USERPROFILE%\Documents\mcp-abap-adt\service-keys; Unix: ~/.config/...)
664
+ const serviceKeyDir = (0, stores_js_1.getPlatformPaths)('service-keys')[0];
665
+ const help = `Usage: mcp-abap-adt-proxy [options]
666
+
667
+ Authorization proxy for MCP clients (Cline, Claude Code, etc.) that cannot
668
+ authenticate on their own to MCP servers deployed on SAP BTP.
669
+
670
+ CONFIGURATION
671
+ -c, --config <path> Load config from YAML/JSON file. CLI flags below
672
+ override values from the file.
673
+
674
+ AUTHENTICATION (BTP / XSUAA)
675
+ --btp <destination> BTP destination name. Service key is read from
676
+ ${serviceKeyDir}${require('node:path').sep}<dest>.json
677
+ (override the base dir with AUTH_BROKER_PATH)
678
+ --target-url <url> Override target service URL (default: from service key)
679
+ --url <url> Alias for --target-url
680
+ --unsafe Persist session/tokens to disk (default: in-memory)
681
+ --header <key=value> Inject a default header into every forwarded
682
+ request. Repeatable. Client-supplied headers win.
683
+
684
+ BROWSER AUTHORIZATION
685
+ --browser <mode> Browser used for OAuth login. One of:
686
+ system - OS default browser (default)
687
+ chrome - Google Chrome
688
+ edge - Microsoft Edge
689
+ firefox - Firefox
690
+ none - Print URL to console; complete login
691
+ via callback OR by pasting the code
692
+ (for SSH/headless/remote sessions)
693
+ headless - Alias for 'none'
694
+ --browser-auth-port <port> Local OAuth callback port. Must match the
695
+ redirect_uri registered on XSUAA (default: 3333).
696
+
697
+ Run without auto-opening a browser (prints the URL, then completes via the
698
+ localhost callback, the paste form at http://<host>:<port>/, or by pasting
699
+ the code in the terminal):
700
+ mcp-abap-adt-proxy --btp <dest> --browser none --browser-auth-port 3333
701
+
702
+ TRANSPORT
703
+ --transport <type> stdio | streamable-http | http | sse
704
+ (auto-detected from stdin TTY if not set)
705
+ --stdio Shortcut for --transport stdio
706
+ --http Shortcut for --transport streamable-http
707
+ --sse Shortcut for --transport sse
708
+ --http-port <port> HTTP transport port (default: 3001)
709
+ --http-host <host> HTTP transport host (default: 127.0.0.1)
710
+ --sse-port <port> SSE transport port (default: 3002)
711
+ --sse-host <host> SSE transport host (default: 127.0.0.1)
712
+
713
+ -h, --help Show this help and exit
714
+
715
+ ENVIRONMENT VARIABLES
716
+ MCP_TRANSPORT, MCP_HTTP_PORT, MCP_HTTP_HOST, MCP_SSE_PORT, MCP_SSE_HOST,
717
+ LOG_LEVEL, MCP_PROXY_UNSAFE, MCP_PROXY_MAX_RETRIES, MCP_PROXY_RETRY_DELAY,
718
+ MCP_PROXY_REQUEST_TIMEOUT, MCP_PROXY_CIRCUIT_BREAKER_THRESHOLD,
719
+ MCP_PROXY_CIRCUIT_BREAKER_TIMEOUT
720
+
721
+ See docs/CONFIGURATION.md for the full configuration reference.
722
+ `;
723
+ process.stdout.write(help);
724
+ }
570
725
  // Auto-start server when run directly (unless MCP_SKIP_AUTO_START is set)
571
726
  if (process.env.MCP_SKIP_AUTO_START !== 'true') {
727
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
728
+ printHelp();
729
+ process.exit(0);
730
+ }
572
731
  const server = new McpAbapAdtProxyServer();
732
+ // Graceful shutdown: release the OAuth callback port, MCP server, and HTTP listener.
733
+ let shuttingDown = false;
734
+ const onSignal = (signal) => {
735
+ if (shuttingDown)
736
+ return;
737
+ shuttingDown = true;
738
+ logger_js_1.logger?.info('Received shutdown signal, releasing resources', { signal });
739
+ server
740
+ .shutdown()
741
+ .catch((error) => {
742
+ logger_js_1.logger?.error('Error during shutdown', {
743
+ error: error instanceof Error ? error.message : String(error),
744
+ });
745
+ })
746
+ .finally(() => process.exit(0));
747
+ };
748
+ process.on('SIGINT', onSignal);
749
+ process.on('SIGTERM', onSignal);
573
750
  server.run().catch((error) => {
574
751
  logger_js_1.logger?.error('Fatal error while running MCP proxy server', {
575
752
  type: 'SERVER_FATAL_ERROR',
@@ -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,CAqC3D;AAED;;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;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"}
@@ -56,24 +56,17 @@ function loadConfig(configPath) {
56
56
  // Get config path from parameter or command line (--config/-c)
57
57
  // Do NOT use environment variable MCP_PROXY_CONFIG - only explicit --config parameter
58
58
  const finalConfigPath = configPath || getConfigPath();
59
- // If --config is provided, load ONLY from that file (no merge with command line params)
59
+ // If --config is provided: load from file, apply defaults, then overlay explicit CLI flags.
60
+ // CLI flags override YAML values (YAML is just the baseline).
60
61
  if (finalConfigPath) {
61
- // Warn if other CLI parameters are also provided (they will be ignored)
62
- const conflictingParams = ['--btp', '--unsafe'].filter((param) => hasArg(param));
63
- if (conflictingParams.length > 0) {
64
- console.warn(`Warning: --config is specified, but the following CLI parameters will be ignored: ${conflictingParams.join(', ')}. ` +
65
- `Configuration will be loaded ONLY from ${finalConfigPath}. ` +
66
- `To use CLI parameters, do not specify --config.`);
67
- }
68
62
  try {
69
- if (fs.existsSync(finalConfigPath)) {
70
- const fileConfig = loadConfigFile(finalConfigPath);
71
- // Apply defaults for missing values in file config
72
- return applyDefaults(fileConfig);
73
- }
74
- else {
63
+ if (!fs.existsSync(finalConfigPath)) {
75
64
  throw new Error(`Config file not found: ${finalConfigPath}`);
76
65
  }
66
+ const fileConfig = loadConfigFile(finalConfigPath);
67
+ const base = applyDefaults(fileConfig);
68
+ const cli = readCliOverrides();
69
+ return mergeCliOverrides(base, cli, finalConfigPath);
77
70
  }
78
71
  catch (error) {
79
72
  throw new Error(`Failed to load config from file ${finalConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
@@ -82,6 +75,61 @@ function loadConfig(configPath) {
82
75
  // No --config provided: load ONLY from command line parameters and environment variables
83
76
  return loadFromEnv();
84
77
  }
78
+ /**
79
+ * Read CLI flags that should override file/YAML configuration.
80
+ * Only includes keys that were explicitly provided on the command line.
81
+ */
82
+ function readCliOverrides() {
83
+ const overrides = {};
84
+ const btp = getArgValue('--btp');
85
+ if (btp !== undefined)
86
+ overrides.btpDestination = btp;
87
+ const targetUrl = getArgValue('--target-url') ?? getArgValue('--url');
88
+ if (targetUrl !== undefined)
89
+ overrides.targetUrl = targetUrl;
90
+ if (hasArg('--unsafe'))
91
+ overrides.unsafe = true;
92
+ const browser = getArgValue('--browser');
93
+ if (browser !== undefined) {
94
+ overrides.browser = browser;
95
+ }
96
+ const browserAuthPortStr = getArgValue('--browser-auth-port');
97
+ if (browserAuthPortStr !== undefined) {
98
+ overrides.browserAuthPort = parseInt(browserAuthPortStr, 10);
99
+ }
100
+ const headerArgs = getAllArgValues('--header');
101
+ if (headerArgs.length > 0) {
102
+ const headers = {};
103
+ for (const arg of headerArgs) {
104
+ const eqIndex = arg.indexOf('=');
105
+ if (eqIndex > 0) {
106
+ const key = arg.substring(0, eqIndex).toLowerCase();
107
+ const value = arg.substring(eqIndex + 1);
108
+ headers[key] = value;
109
+ }
110
+ }
111
+ overrides.defaultHeaders = headers;
112
+ }
113
+ return overrides;
114
+ }
115
+ /**
116
+ * Merge CLI overrides on top of file-based config. Logs which keys were overridden.
117
+ * `defaultHeaders` is merged per-key (CLI keys override YAML keys); other fields are replaced.
118
+ */
119
+ function mergeCliOverrides(base, cli, configPath) {
120
+ const merged = { ...base, ...cli };
121
+ if (cli.defaultHeaders) {
122
+ merged.defaultHeaders = {
123
+ ...(base.defaultHeaders || {}),
124
+ ...cli.defaultHeaders,
125
+ };
126
+ }
127
+ const overriddenKeys = Object.keys(cli);
128
+ if (overriddenKeys.length > 0) {
129
+ console.warn(`Note: CLI flags override values from ${configPath}: ${overriddenKeys.join(', ')}`);
130
+ }
131
+ return merged;
132
+ }
85
133
  /**
86
134
  * Extract transport-related fields from a raw config object
87
135
  */
@@ -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,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"}
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;IAwB3D;;;;OAIG;YACW,wBAAwB;IA0FtC;;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;IAStB;;;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;CA0B7E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAEnB"}
@@ -155,8 +155,9 @@ class BtpProxy {
155
155
  destination,
156
156
  error: error instanceof Error ? error.message : String(error),
157
157
  });
158
- // We don't throw here to avoid crashing the server on startup,
159
- // but the user will see the error in logs/stderr (via getJwtToken)
158
+ // Re-throw so the server can decide to exit (auth requested but not
159
+ // completed) instead of running in a permanently unauthenticated state.
160
+ throw error;
160
161
  }
161
162
  }
162
163
  /**
@@ -199,9 +200,10 @@ class BtpProxy {
199
200
  });
200
201
  }
201
202
  if (!authConfig) {
203
+ const serviceKeyDir = (0, stores_js_1.getPlatformPaths)('service-keys')[0];
202
204
  logger_js_1.logger?.error('Service key not found for destination', {
203
205
  destination,
204
- hint: 'Ensure service key file exists in ~/.config/mcp-abap-adt/service-keys/',
206
+ hint: `Ensure service key file "${destination}.json" exists in ${serviceKeyDir} (override the base dir with AUTH_BROKER_PATH)`,
205
207
  });
206
208
  // We cannot proceed without config, but we'll let it fail with a clear message
207
209
  // Or we could throw here.
@@ -213,6 +215,10 @@ class BtpProxy {
213
215
  const providerConfig = {
214
216
  browser: this.config.browser,
215
217
  redirectPort: this.config.browserAuthPort || 3333,
218
+ // Pass a logger so browserAuth can surface the "Open this URL" prompt in
219
+ // 'none'/'headless' mode. Without it, startBrowserAuth gets a null logger
220
+ // and silently drops the authorization URL.
221
+ logger: loggerAdapter,
216
222
  ...(authConfig
217
223
  ? {
218
224
  uaaUrl: authConfig.uaaUrl,
@@ -474,6 +480,8 @@ class BtpProxy {
474
480
  clearTimeout(timer);
475
481
  }
476
482
  this.refreshTimers.clear();
483
+ this.tokenCache.clear();
484
+ this.btpAuthBrokers.clear();
477
485
  }
478
486
  /**
479
487
  * Get the target service URL for a destination.
@@ -922,6 +930,7 @@ class BtpProxy {
922
930
  clientSecret: 'placeholder',
923
931
  browser: loadedConfig.browser,
924
932
  redirectPort: loadedConfig.browserAuthPort,
933
+ logger: loggerAdapter,
925
934
  }),
926
935
  }, 'none', loggerAdapter);
927
936
  return new BtpProxy(defaultBtpAuthBroker, loadedConfig);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/proxy",
3
- "version": "1.2.1",
3
+ "version": "1.5.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",
@@ -63,8 +63,8 @@
63
63
  "typescript": "^6.0.3"
64
64
  },
65
65
  "dependencies": {
66
- "@mcp-abap-adt/auth-broker": "^1.0.5",
67
- "@mcp-abap-adt/auth-providers": "^1.0.5",
66
+ "@mcp-abap-adt/auth-broker": "^1.0.6",
67
+ "@mcp-abap-adt/auth-providers": "^1.1.0",
68
68
  "@mcp-abap-adt/auth-stores": "^1.0.4",
69
69
  "@mcp-abap-adt/header-validator": "^0.1.8",
70
70
  "@mcp-abap-adt/interfaces": "^7.0.0",