@mcp-abap-adt/proxy 0.1.3 → 0.1.5

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,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.4] - 2025-12-02
11
+
12
+ ### Added
13
+ - **Documentation for BTP-Only authentication mode**
14
+ - Added "Scenario 2: BTP Authentication Only (No SAP Configuration)" in `doc/USAGE.md`
15
+ - Documented use case for connecting to any BTP service without SAP ABAP configuration
16
+ - Explained that `x-mcp-destination` is optional when using `x-btp-destination` or `--btp`
17
+ - Updated README.md with "BTP-Only Mode" section explaining behavior when only `--btp` is provided
18
+ - **Enhanced help text in `bin/mcp-abap-adt-proxy.js`**
19
+ - Added detailed "Usage Modes" section with three modes:
20
+ 1. BTP Authentication Mode (with `--btp` only)
21
+ 2. BTP + SAP Configuration Mode (`--btp + --mcp`)
22
+ 3. Local Testing Mode (only `--mcp` or `--mcp-url`)
23
+ - Added examples for each usage mode
24
+ - Added information about headers for HTTP/SSE transports
25
+ - Added service keys location and structure documentation
26
+ - Clarified that `abap.url` in service key is used as MCP server URL (even for non-SAP services)
27
+
28
+ ### Documentation
29
+ - Improved clarity on when to use each authentication mode
30
+ - Added examples showing BTP-only usage (without SAP configuration)
31
+ - Documented that proxy works with any BTP service, not just SAP ABAP
32
+
10
33
  ## [0.1.3] - 2025-12-02
11
34
 
12
35
  ### Fixed
package/README.md CHANGED
@@ -81,9 +81,15 @@ The proxy uses two service keys (URLs are obtained from service keys):
81
81
 
82
82
  **BTP Authentication Mode** (with `x-btp-destination` or `--btp`):
83
83
  1. `x-btp-destination` (or `--btp`) → Gets JWT token from auth-broker → Adds `Authorization: Bearer <token>` header
84
- 2. `x-mcp-destination` (or `--mcp`) → Gets JWT token and SAP config from auth-broker → Adds SAP headers (`x-sap-jwt-token`, `x-sap-url`, etc.)
84
+ 2. `x-mcp-destination` (or `--mcp`) → Gets JWT token and SAP config from auth-broker → Adds SAP headers (`x-sap-jwt-token`, `x-sap-url`, etc.) - **optional**
85
85
  3. MCP server URL obtained from service key for `x-btp-destination`
86
86
 
87
+ **BTP-Only Mode** (with only `x-btp-destination`/`--btp`, without `x-mcp-destination`):
88
+ - Works with any BTP service, not just SAP ABAP
89
+ - Only adds `Authorization: Bearer <token>` header (no SAP headers)
90
+ - MCP server URL obtained from BTP destination service key
91
+ - Suitable for BTP services that don't require SAP ABAP configuration
92
+
87
93
  **Local Testing Mode** (with only `x-mcp-destination`/`--mcp` or `x-mcp-url`/`--mcp-url`):
88
94
  1. `x-mcp-url` (or `--mcp-url`) → Direct URL to MCP server (no authentication)
89
95
  2. `x-mcp-destination` (or `--mcp`) → Gets MCP server URL from service key for MCP destination (optional token)
@@ -96,6 +102,8 @@ The proxy uses two service keys (URLs are obtained from service keys):
96
102
  - **[API Documentation](./doc/API.md)** - API reference and interfaces
97
103
  - **[Architecture](./doc/ARCHITECTURE.md)** - System architecture and design
98
104
  - **[Troubleshooting](./doc/TROUBLESHOOTING.md)** - Common issues and solutions
105
+ - **[Routing Logic Specification](./doc/ROUTING_LOGIC.md)** - Detailed routing logic and scenarios
106
+ - **[Implementation Analysis](./doc/IMPLEMENTATION_ANALYSIS.md)** - Current implementation analysis
99
107
  - **[Roadmap](./ROADMAP.md)** - Development roadmap and progress
100
108
 
101
109
  ## How It Works
@@ -94,6 +94,49 @@ Transport Modes:
94
94
  - http / streamable-http: HTTP server (default if stdin is TTY, port 3001)
95
95
  - sse: Server-Sent Events server (port 3002)
96
96
 
97
+ Usage Modes:
98
+
99
+ 1. BTP Authentication Mode (with --btp):
100
+ - Adds Authorization: Bearer <token> header from BTP destination
101
+ - Gets MCP server URL from BTP destination service key
102
+ - Works with any BTP service, not just SAP ABAP
103
+ Example: mcp-abap-adt-proxy --btp=ai
104
+
105
+ 2. BTP + SAP Configuration Mode (--btp + --mcp):
106
+ - Adds Authorization: Bearer <token> header from BTP destination
107
+ - Adds SAP headers (x-sap-jwt-token, x-sap-url, etc.) from MCP destination
108
+ - Gets MCP server URL from BTP destination service key
109
+ - Suitable for SAP ABAP systems that require both BTP and SAP authentication
110
+ Example: mcp-abap-adt-proxy --btp=ai --mcp=trial
111
+
112
+ 3. Local Testing Mode (only --mcp or --mcp-url):
113
+ - No BTP authentication required
114
+ - Gets MCP server URL from MCP destination service key (--mcp) or uses direct URL (--mcp-url)
115
+ - Optional SAP token (won't fail if unavailable)
116
+ - Enables local integration testing without BTP authentication
117
+ Example: mcp-abap-adt-proxy --mcp=trial
118
+ Example: mcp-abap-adt-proxy --mcp-url=http://localhost:3000/mcp
119
+
120
+ Headers (for HTTP/SSE transports):
121
+ - x-btp-destination: Destination for BTP Cloud authorization (optional if --btp is used)
122
+ - x-mcp-destination: Destination for SAP ABAP connection (optional if --mcp is used)
123
+ - x-mcp-url: Direct MCP server URL (optional if --mcp-url is used)
124
+
125
+ At least one of the above headers (or corresponding --btp/--mcp/--mcp-url parameter) is required.
126
+
127
+ Service Keys:
128
+ Service keys should be placed in:
129
+ - Unix: ~/.config/mcp-abap-adt/service-keys/<destination>.json
130
+ - Windows: %USERPROFILE%\\Documents\\mcp-abap-adt\\service-keys\\<destination>.json
131
+
132
+ For BTP destination, service key should contain:
133
+ {
134
+ "uaa": { "url": "...", "clientid": "...", "clientsecret": "..." },
135
+ "abap": { "url": "https://your-mcp-server.com" }
136
+ }
137
+
138
+ The "abap.url" field is used as the MCP server URL (even for non-SAP services).
139
+
97
140
  For more information, see: https://github.com/fr0ster/mcp-abap-adt-proxy
98
141
  `);
99
142
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;GAWG;AAOH,OAAO,EAAwB,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAOjF;;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,gBAAgB,CAAC,CAAmB;gBAEhC,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAqClE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;;OAGG;YACW,kBAAkB;IA4EhC;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAkD1B;;OAEG;YACW,eAAe;IAsH7B;;OAEG;YACW,kBAAkB;IAsEhC;;OAEG;YACW,cAAc;IA+R5B;;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":";AAEA;;;;;;;;;;;GAWG;AAOH,OAAO,EAAwB,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAOjF;;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,gBAAgB,CAAC,CAAmB;gBAEhC,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAqClE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;;OAGG;YACW,kBAAkB;IA4EhC;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAkD1B;;OAEG;YACW,eAAe;IAsH7B;;OAEG;YACW,kBAAkB;IA6EhC;;OAEG;YACW,cAAc;IA+R5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAmBhC;AAGD,eAAe,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -341,11 +341,16 @@ class McpAbapAdtProxyServer {
341
341
  });
342
342
  }
343
343
  catch (error) {
344
+ const errorMessage = error instanceof Error ? error.message : String(error);
344
345
  logger_js_1.logger.error("Failed to handle cloud-llm-hub proxy request", {
345
346
  type: "CLOUD_LLM_HUB_PROXY_ERROR",
346
- error: error instanceof Error ? error.message : String(error),
347
+ error: errorMessage,
347
348
  stack: error instanceof Error ? error.stack : undefined,
348
349
  });
350
+ // Output error to stderr for user visibility (only if verbose mode is enabled)
351
+ if ((0, cloudLlmHubProxy_js_1.shouldWriteStderr)()) {
352
+ process.stderr.write(`[MCP Proxy] ✗ Connection error: ${errorMessage}\n`);
353
+ }
349
354
  if (!res.headersSent) {
350
355
  res.writeHead(500, { "Content-Type": "application/json" });
351
356
  res.end(JSON.stringify({
@@ -353,7 +358,7 @@ class McpAbapAdtProxyServer {
353
358
  id: intercepted.body?.id || null,
354
359
  error: {
355
360
  code: -32000,
356
- message: error instanceof Error ? error.message : "Unknown error",
361
+ message: errorMessage,
357
362
  },
358
363
  }));
359
364
  }
@@ -643,3 +648,28 @@ class McpAbapAdtProxyServer {
643
648
  exports.McpAbapAdtProxyServer = McpAbapAdtProxyServer;
644
649
  // Export for use in bin script
645
650
  exports.default = McpAbapAdtProxyServer;
651
+ // Auto-start server when run directly (unless MCP_SKIP_AUTO_START is set)
652
+ if (process.env.MCP_SKIP_AUTO_START !== "true") {
653
+ const server = new McpAbapAdtProxyServer();
654
+ server.run().catch((error) => {
655
+ logger_js_1.logger.error("Fatal error while running MCP proxy server", {
656
+ type: "SERVER_FATAL_ERROR",
657
+ error: error instanceof Error ? error.message : String(error),
658
+ });
659
+ // Always write fatal errors to stderr (even in non-verbose mode, as these are critical)
660
+ // But skip in test environment
661
+ const isTestEnv = process.env.NODE_ENV === "test" ||
662
+ process.env.JEST_WORKER_ID !== undefined ||
663
+ typeof globalThis.jest !== "undefined";
664
+ if (!isTestEnv) {
665
+ process.stderr.write(`[MCP Proxy] ✗ Fatal error: ${error instanceof Error ? error.message : String(error)}\n`);
666
+ }
667
+ // On Windows, add a small delay before exit to allow error message to be visible
668
+ if (process.platform === 'win32') {
669
+ setTimeout(() => process.exit(1), 100);
670
+ }
671
+ else {
672
+ process.exit(1);
673
+ }
674
+ });
675
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,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,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,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;CAChC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAoC3D;AA+ED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA6C5G"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,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,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,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;CAChC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAoC3D;AA+ED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA+C5G"}
@@ -157,12 +157,13 @@ function mergeConfig(fileConfig, envConfig) {
157
157
  function validateConfig(config) {
158
158
  const errors = [];
159
159
  const warnings = [];
160
- // Cloud LLM Hub URL is only required if we're using proxy functionality
161
- // For direct cloud and basic auth, it's not needed
162
- if (!config.cloudLlmHubUrl) {
163
- warnings.push("CLOUD_LLM_HUB_URL is not set - proxy to cloud-llm-hub will not work");
160
+ // Cloud LLM Hub URL is only required if we're not using BTP/MCP destinations
161
+ // If --btp, --mcp, or --mcp-url is provided, URL will be obtained from service keys
162
+ const hasDestination = config.btpDestination || config.mcpDestination || config.mcpUrl;
163
+ if (!config.cloudLlmHubUrl && !hasDestination) {
164
+ warnings.push("CLOUD_LLM_HUB_URL is not set and no destination provided (--btp, --mcp, or --mcp-url) - proxy will not work without a destination");
164
165
  }
165
- else {
166
+ else if (config.cloudLlmHubUrl) {
166
167
  try {
167
168
  new URL(config.cloudLlmHubUrl);
168
169
  }
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,oBAAY,QAAQ;IAClB,KAAK,IAAI;IACT,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;CACV;AAED,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9D;AAqCD,eAAO,MAAM,MAAM,EAAE,MAIpB,CAAC"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,oBAAY,QAAQ;IAClB,KAAK,IAAI;IACT,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;CACV;AAED,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9D;AAiDD,eAAO,MAAM,MAAM,EAAE,MAIpB,CAAC"}
@@ -13,10 +13,21 @@ var LogLevel;
13
13
  })(LogLevel || (exports.LogLevel = LogLevel = {}));
14
14
  class SimpleLogger {
15
15
  level;
16
+ verbose;
16
17
  constructor(level = LogLevel.INFO) {
17
18
  this.level = level;
19
+ // Enable logging only if MCP_PROXY_VERBOSE or DEBUG environment variable is set
20
+ // In default mode (without env vars), no logs are output
21
+ this.verbose = process.env.MCP_PROXY_VERBOSE === "true" ||
22
+ process.env.DEBUG === "true" ||
23
+ process.env.DEBUG?.includes("mcp-proxy") === true;
18
24
  }
19
25
  log(level, prefix, message, meta) {
26
+ // In non-verbose mode, don't output any logs
27
+ if (!this.verbose) {
28
+ return;
29
+ }
30
+ // In verbose mode, respect the log level
20
31
  if (level < this.level) {
21
32
  return;
22
33
  }
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * Platform-specific stores for proxy
3
- * Uses FileServiceKeyStore and either FileSessionStore or SafeSessionStore
3
+ * Uses AbapServiceKeyStore/XsuaaServiceKeyStore and either AbapSessionStore or SafeAbapSessionStore
4
4
  * depending on the unsafe configuration parameter
5
5
  */
6
- import { IServiceKeyStore, ISessionStore } from "@mcp-abap-adt/auth-broker";
6
+ import type { IServiceKeyStore, ISessionStore } from "@mcp-abap-adt/auth-broker";
7
7
  /**
8
8
  * Get platform-specific stores
9
9
  * Returns stores based on configuration:
10
- * - If unsafe=true: uses FileSessionStore (persists to disk)
11
- * - If unsafe=false: uses SafeSessionStore (in-memory, secure)
12
- * @param unsafe If true, use FileSessionStore. If false, use SafeSessionStore (default).
10
+ * - If unsafe=true: uses AbapSessionStore (persists to disk)
11
+ * - If unsafe=false: uses SafeAbapSessionStore (in-memory, secure)
12
+ * - Service key store: Combined store that tries both standard and XSUAA loaders
13
+ * @param unsafe If true, use AbapSessionStore. If false, use SafeAbapSessionStore (default).
13
14
  */
14
15
  export declare function getPlatformStores(unsafe?: boolean): Promise<{
15
16
  serviceKeyStore: IServiceKeyStore;
@@ -1 +1 @@
1
- {"version":3,"file":"stores.d.ts","sourceRoot":"","sources":["../../src/lib/stores.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAIL,gBAAgB,EAChB,aAAa,EACd,MAAM,2BAA2B,CAAC;AAEnC;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC;IACxE,eAAe,EAAE,gBAAgB,CAAC;IAClC,YAAY,EAAE,aAAa,CAAC;CAC7B,CAAC,CAKD"}
1
+ {"version":3,"file":"stores.d.ts","sourceRoot":"","sources":["../../src/lib/stores.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EAId,MAAM,2BAA2B,CAAC;AAuHnC;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC;IACxE,eAAe,EAAE,gBAAgB,CAAC;IAClC,YAAY,EAAE,aAAa,CAAC;CAC7B,CAAC,CAeD"}
@@ -1,22 +1,169 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Platform-specific stores for proxy
4
- * Uses FileServiceKeyStore and either FileSessionStore or SafeSessionStore
4
+ * Uses AbapServiceKeyStore/XsuaaServiceKeyStore and either AbapSessionStore or SafeAbapSessionStore
5
5
  * depending on the unsafe configuration parameter
6
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
+ })();
7
40
  Object.defineProperty(exports, "__esModule", { value: true });
8
41
  exports.getPlatformStores = getPlatformStores;
9
- const auth_broker_1 = require("@mcp-abap-adt/auth-broker");
42
+ const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
43
+ const path = __importStar(require("path"));
44
+ const os = __importStar(require("os"));
45
+ /**
46
+ * Get platform-specific default paths for service keys and sessions
47
+ *
48
+ * Priority:
49
+ * 1. AUTH_BROKER_PATH environment variable
50
+ * 2. Platform-specific standard paths:
51
+ * - Unix: ~/.config/mcp-abap-adt/service-keys
52
+ * - Windows: %USERPROFILE%\Documents\mcp-abap-adt\service-keys
53
+ * 3. Current working directory (process.cwd())
54
+ *
55
+ * @param subfolder Subfolder name ('service-keys' or 'sessions')
56
+ * @returns Array of resolved absolute paths
57
+ */
58
+ function getPlatformPaths(subfolder) {
59
+ const paths = [];
60
+ const isWindows = process.platform === 'win32';
61
+ // Priority 1: AUTH_BROKER_PATH environment variable
62
+ const envPath = process.env.AUTH_BROKER_PATH;
63
+ if (envPath) {
64
+ // Support both colon (Unix) and semicolon (Windows) separators
65
+ const envPaths = envPath.split(/[:;]/).map(p => p.trim()).filter(p => p.length > 0);
66
+ paths.push(...envPaths.map(p => path.resolve(p)));
67
+ }
68
+ // Priority 2: Platform-specific standard paths (always added, unless AUTH_BROKER_PATH is set)
69
+ // If AUTH_BROKER_PATH is set, platform paths are still added but with lower priority
70
+ const homeDir = os.homedir();
71
+ if (isWindows) {
72
+ // Windows: %USERPROFILE%\Documents\mcp-abap-adt\{subfolder}
73
+ const basePath = path.join(homeDir, 'Documents', 'mcp-abap-adt');
74
+ if (subfolder) {
75
+ paths.push(path.join(basePath, subfolder));
76
+ }
77
+ else {
78
+ paths.push(basePath);
79
+ }
80
+ }
81
+ else {
82
+ // Unix (Linux/macOS): ~/.config/mcp-abap-adt/{subfolder}
83
+ const basePath = path.join(homeDir, '.config', 'mcp-abap-adt');
84
+ if (subfolder) {
85
+ paths.push(path.join(basePath, subfolder));
86
+ }
87
+ else {
88
+ paths.push(basePath);
89
+ }
90
+ }
91
+ // Priority 3: Current working directory (always added as fallback)
92
+ paths.push(process.cwd());
93
+ // Remove duplicates while preserving order
94
+ const uniquePaths = [];
95
+ const seen = new Set();
96
+ for (const p of paths) {
97
+ const normalized = path.normalize(p);
98
+ if (!seen.has(normalized)) {
99
+ seen.add(normalized);
100
+ uniquePaths.push(normalized);
101
+ }
102
+ }
103
+ return uniquePaths;
104
+ }
105
+ /**
106
+ * Combined service key store that tries both standard and XSUAA loaders
107
+ * Tries standard ABAP service key loader first, then XSUAA loader if not found
108
+ *
109
+ * Note: Stores only support a single directory, so we use the first path from the array
110
+ */
111
+ class CombinedServiceKeyStore {
112
+ abapStore;
113
+ xsuaaStore;
114
+ constructor(searchPaths) {
115
+ // Stores only support a single directory, use the first path
116
+ const firstPath = searchPaths[0] || process.cwd();
117
+ this.abapStore = new auth_stores_1.AbapServiceKeyStore(firstPath);
118
+ this.xsuaaStore = new auth_stores_1.XsuaaServiceKeyStore(firstPath);
119
+ }
120
+ async getServiceKey(destination) {
121
+ // Try ABAP service key store first
122
+ const abapKey = await this.abapStore.getServiceKey(destination);
123
+ if (abapKey) {
124
+ return abapKey;
125
+ }
126
+ // If not found, try XSUAA store (for BTP XSUAA service keys)
127
+ return await this.xsuaaStore.getServiceKey(destination);
128
+ }
129
+ async getAuthorizationConfig(destination) {
130
+ // Try ABAP service key store first
131
+ const abapConfig = await this.abapStore.getAuthorizationConfig(destination);
132
+ if (abapConfig) {
133
+ return abapConfig;
134
+ }
135
+ // If not found, try XSUAA store (for BTP XSUAA service keys)
136
+ return await this.xsuaaStore.getAuthorizationConfig(destination);
137
+ }
138
+ async getConnectionConfig(destination) {
139
+ // Try ABAP service key store first
140
+ const abapConfig = await this.abapStore.getConnectionConfig(destination);
141
+ if (abapConfig) {
142
+ return abapConfig;
143
+ }
144
+ // If not found, try XSUAA store (for BTP XSUAA service keys)
145
+ return await this.xsuaaStore.getConnectionConfig(destination);
146
+ }
147
+ }
10
148
  /**
11
149
  * Get platform-specific stores
12
150
  * Returns stores based on configuration:
13
- * - If unsafe=true: uses FileSessionStore (persists to disk)
14
- * - If unsafe=false: uses SafeSessionStore (in-memory, secure)
15
- * @param unsafe If true, use FileSessionStore. If false, use SafeSessionStore (default).
151
+ * - If unsafe=true: uses AbapSessionStore (persists to disk)
152
+ * - If unsafe=false: uses SafeAbapSessionStore (in-memory, secure)
153
+ * - Service key store: Combined store that tries both standard and XSUAA loaders
154
+ * @param unsafe If true, use AbapSessionStore. If false, use SafeAbapSessionStore (default).
16
155
  */
17
156
  async function getPlatformStores(unsafe = false) {
157
+ // Get platform-specific paths for service keys and sessions
158
+ const serviceKeyPaths = getPlatformPaths('service-keys');
159
+ const sessionPaths = getPlatformPaths('sessions');
160
+ // Stores only support a single directory, use the first path
161
+ const firstServiceKeyPath = serviceKeyPaths[0] || process.cwd();
162
+ const firstSessionPath = sessionPaths[0] || process.cwd();
18
163
  return {
19
- serviceKeyStore: new auth_broker_1.FileServiceKeyStore(),
20
- sessionStore: unsafe ? new auth_broker_1.FileSessionStore() : new auth_broker_1.SafeSessionStore(),
164
+ serviceKeyStore: new CombinedServiceKeyStore(serviceKeyPaths),
165
+ sessionStore: unsafe
166
+ ? new auth_stores_1.AbapSessionStore(firstSessionPath)
167
+ : new auth_stores_1.SafeAbapSessionStore(),
21
168
  };
22
169
  }
@@ -7,6 +7,11 @@
7
7
  import { AuthBroker } from "@mcp-abap-adt/auth-broker";
8
8
  import { RoutingDecision } from "../router/headerAnalyzer.js";
9
9
  import { ProxyConfig } from "../lib/config.js";
10
+ /**
11
+ * Check if error messages should be written to stderr
12
+ * Only output in verbose mode and not in test environment
13
+ */
14
+ export declare function shouldWriteStderr(): boolean;
10
15
  export interface ProxyRequest {
11
16
  method: string;
12
17
  params?: any;
@@ -1 +1 @@
1
- {"version":3,"file":"cloudLlmHubProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/cloudLlmHubProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAc,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAU3D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,GAAG,CAAC;KACZ,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,UAAU,CAAgE;IAClF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAc;gBAEhB,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAmEzF;;OAEG;YACW,WAAW;IAwDzB;;;;;;;;OAQG;YACW,iBAAiB;IA4J/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;CA2G1B;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,cAAc,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CAY3B"}
1
+ {"version":3,"file":"cloudLlmHubProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/cloudLlmHubProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAGvD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAc,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAU3D;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAQ3C;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,GAAG,CAAC;KACZ,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,UAAU,CAAgE;IAClF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAc;gBAEhB,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IA2EzF;;OAEG;YACW,WAAW;IA6DzB;;;;;;;;OAQG;YACW,iBAAiB;IAkL/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;CAuH1B;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,cAAc,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CAc3B"}
@@ -10,13 +10,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.CloudLlmHubProxy = void 0;
13
+ exports.shouldWriteStderr = shouldWriteStderr;
13
14
  exports.createCloudLlmHubProxy = createCloudLlmHubProxy;
14
15
  const axios_1 = __importDefault(require("axios"));
15
16
  const auth_broker_1 = require("@mcp-abap-adt/auth-broker");
17
+ const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
16
18
  const logger_js_1 = require("../lib/logger.js");
17
19
  const config_js_1 = require("../lib/config.js");
18
20
  const stores_js_1 = require("../lib/stores.js");
19
21
  const errorHandler_js_1 = require("../lib/errorHandler.js");
22
+ /**
23
+ * Check if error messages should be written to stderr
24
+ * Only output in verbose mode and not in test environment
25
+ */
26
+ function shouldWriteStderr() {
27
+ const verboseMode = process.env.MCP_PROXY_VERBOSE === "true" ||
28
+ process.env.DEBUG === "true" ||
29
+ process.env.DEBUG?.includes("mcp-proxy") === true;
30
+ const isTestEnv = process.env.NODE_ENV === "test" ||
31
+ process.env.JEST_WORKER_ID !== undefined ||
32
+ typeof globalThis.jest !== "undefined";
33
+ return verboseMode && !isTestEnv;
34
+ }
20
35
  /**
21
36
  * Cloud LLM Hub Proxy Client
22
37
  */
@@ -40,11 +55,19 @@ class CloudLlmHubProxy {
40
55
  // Initialize circuit breaker
41
56
  this.circuitBreaker = new errorHandler_js_1.CircuitBreaker(this.config.circuitBreakerThreshold || 5, this.config.circuitBreakerTimeout || 60000);
42
57
  // Create axios instance without baseURL - we'll use full URLs from x-mcp-url
58
+ // Configure HTTPS agent for proper SSL/TLS handling
59
+ const https = require('https');
43
60
  this.axiosInstance = axios_1.default.create({
44
61
  timeout: this.config.requestTimeout || 60000,
45
62
  headers: {
46
63
  "Content-Type": "application/json",
47
64
  },
65
+ httpsAgent: new https.Agent({
66
+ // Allow self-signed certificates if needed (for development)
67
+ rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0',
68
+ // Keep connections alive for better performance
69
+ keepAlive: true,
70
+ }),
48
71
  });
49
72
  // Add request interceptor for logging
50
73
  this.axiosInstance.interceptors.request.use((config) => {
@@ -124,11 +147,16 @@ class CloudLlmHubProxy {
124
147
  return token;
125
148
  }
126
149
  catch (error) {
150
+ const errorMessage = error instanceof Error ? error.message : String(error);
127
151
  logger_js_1.logger.error("Failed to get JWT token from auth-broker after retries", {
128
152
  type: "JWT_TOKEN_ERROR",
129
153
  destination,
130
- error: error instanceof Error ? error.message : String(error),
154
+ error: errorMessage,
131
155
  });
156
+ // Output error to stderr for user visibility (only if verbose mode is enabled)
157
+ if (shouldWriteStderr()) {
158
+ process.stderr.write(`[MCP Proxy] ✗ Failed to get token for destination "${destination}": ${errorMessage}\n`);
159
+ }
132
160
  throw error;
133
161
  }
134
162
  }
@@ -167,7 +195,8 @@ class CloudLlmHubProxy {
167
195
  // Only used if x-mcp-destination or --mcp is provided
168
196
  // For local testing (without BTP), token retrieval is optional - URL is still needed
169
197
  if (routingDecision.mcpDestination) {
170
- const sapUrl = await this.authBroker.getSapUrl(routingDecision.mcpDestination);
198
+ const connConfig = await this.authBroker.getConnectionConfig(routingDecision.mcpDestination);
199
+ const sapUrl = connConfig?.serviceUrl;
171
200
  // Try to get token, but don't fail if it's not available (for local testing)
172
201
  try {
173
202
  const sapToken = await this.getJwtToken(routingDecision.mcpDestination, forceTokenRefresh);
@@ -225,9 +254,14 @@ class CloudLlmHubProxy {
225
254
  }
226
255
  else if (routingDecision.btpDestination) {
227
256
  // Get URL from BTP destination service key
228
- baseUrl = await this.authBroker.getSapUrl(routingDecision.btpDestination);
257
+ const connConfig = await this.authBroker.getConnectionConfig(routingDecision.btpDestination);
258
+ baseUrl = connConfig?.serviceUrl;
229
259
  if (!baseUrl) {
230
- throw new Error(`Failed to get MCP server URL from BTP destination "${routingDecision.btpDestination}". Check service key file.`);
260
+ const errorMsg = `Failed to get MCP server URL from BTP destination "${routingDecision.btpDestination}". Check service key file.`;
261
+ if (shouldWriteStderr()) {
262
+ process.stderr.write(`[MCP Proxy] ✗ ${errorMsg}\n`);
263
+ }
264
+ throw new Error(errorMsg);
231
265
  }
232
266
  logger_js_1.logger.debug("Using MCP URL from BTP destination service key", {
233
267
  type: "MCP_URL_FROM_BTP",
@@ -237,9 +271,14 @@ class CloudLlmHubProxy {
237
271
  }
238
272
  else if (routingDecision.mcpDestination) {
239
273
  // Get URL from MCP destination service key (for local testing without BTP)
240
- baseUrl = await this.authBroker.getSapUrl(routingDecision.mcpDestination);
274
+ const connConfig = await this.authBroker.getConnectionConfig(routingDecision.mcpDestination);
275
+ baseUrl = connConfig?.serviceUrl;
241
276
  if (!baseUrl) {
242
- throw new Error(`Failed to get MCP server URL from MCP destination "${routingDecision.mcpDestination}". Check service key file.`);
277
+ const errorMsg = `Failed to get MCP server URL from MCP destination "${routingDecision.mcpDestination}". Check service key file.`;
278
+ if (shouldWriteStderr()) {
279
+ process.stderr.write(`[MCP Proxy] ✗ ${errorMsg}\n`);
280
+ }
281
+ throw new Error(errorMsg);
243
282
  }
244
283
  logger_js_1.logger.debug("Using MCP URL from MCP destination service key (local testing mode)", {
245
284
  type: "MCP_URL_FROM_MCP",
@@ -254,9 +293,15 @@ class CloudLlmHubProxy {
254
293
  // If baseUrl already contains the path, use it as-is
255
294
  // Otherwise, append default endpoint /mcp/stream/http
256
295
  let fullUrl;
257
- if (baseUrl.includes("/mcp/") || baseUrl.endsWith("/mcp")) {
258
- // URL already contains MCP path
296
+ // Check if URL already contains MCP path (more flexible check)
297
+ if (baseUrl.includes("/mcp/") || baseUrl.endsWith("/mcp") || baseUrl.includes("/mcp/stream/")) {
298
+ // URL already contains MCP path - use as-is (but remove trailing slash if present)
259
299
  fullUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
300
+ logger_js_1.logger.debug("Using MCP URL as-is (already contains path)", {
301
+ type: "MCP_URL_AS_IS",
302
+ original: baseUrl,
303
+ final: fullUrl,
304
+ });
260
305
  }
261
306
  else {
262
307
  // Append default endpoint
@@ -264,6 +309,11 @@ class CloudLlmHubProxy {
264
309
  fullUrl = baseUrl.endsWith("/")
265
310
  ? `${baseUrl.slice(0, -1)}${mcpPath}`
266
311
  : `${baseUrl}${mcpPath}`;
312
+ logger_js_1.logger.debug("Appended MCP path to base URL", {
313
+ type: "MCP_URL_APPENDED",
314
+ original: baseUrl,
315
+ final: fullUrl,
316
+ });
267
317
  }
268
318
  logger_js_1.logger.debug("Built proxy request", {
269
319
  type: "PROXY_REQUEST_BUILT",
@@ -347,16 +397,27 @@ class CloudLlmHubProxy {
347
397
  });
348
398
  }
349
399
  }
400
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
401
+ const statusCode = axios_1.default.isAxiosError(error) && error.response?.status
402
+ ? error.response.status
403
+ : undefined;
350
404
  logger_js_1.logger.error("Failed to proxy request to cloud-llm-hub", {
351
405
  type: "PROXY_REQUEST_ERROR",
352
- error: error instanceof Error ? error.message : String(error),
353
- status: axios_1.default.isAxiosError(error) ? error.response?.status : undefined,
406
+ error: errorMessage,
407
+ status: statusCode,
354
408
  circuitBreakerState: this.circuitBreaker.getState(),
355
409
  });
410
+ // Output error to stderr for user visibility (only if verbose mode is enabled)
411
+ if (shouldWriteStderr()) {
412
+ if (statusCode) {
413
+ process.stderr.write(`[MCP Proxy] ✗ Connection failed: ${errorMessage} (HTTP ${statusCode})\n`);
414
+ }
415
+ else {
416
+ process.stderr.write(`[MCP Proxy] ✗ Connection failed: ${errorMessage}\n`);
417
+ }
418
+ }
356
419
  // Return error response in MCP format
357
- return (0, errorHandler_js_1.createErrorResponse)(originalRequest.id || null, axios_1.default.isAxiosError(error) && error.response?.status
358
- ? error.response.status
359
- : -32000, error instanceof Error ? error.message : "Unknown error", {
420
+ return (0, errorHandler_js_1.createErrorResponse)(originalRequest.id || null, statusCode || -32000, errorMessage, {
360
421
  circuitBreakerState: this.circuitBreaker.getState(),
361
422
  originalError: axios_1.default.isAxiosError(error) && error.response?.data
362
423
  ? error.response.data
@@ -372,9 +433,11 @@ exports.CloudLlmHubProxy = CloudLlmHubProxy;
372
433
  async function createCloudLlmHubProxy(cloudLlmHubUrl, config) {
373
434
  const unsafe = config?.unsafe ?? false;
374
435
  const { serviceKeyStore, sessionStore } = await (0, stores_js_1.getPlatformStores)(unsafe);
436
+ const tokenProvider = new auth_providers_1.BtpTokenProvider();
375
437
  const authBroker = new auth_broker_1.AuthBroker({
376
438
  serviceKeyStore,
377
439
  sessionStore,
440
+ tokenProvider,
378
441
  }, "system");
379
442
  return new CloudLlmHubProxy(cloudLlmHubUrl, authBroker, config);
380
443
  }
@@ -1 +1 @@
1
- {"version":3,"file":"directCloudRouter.d.ts","sourceRoot":"","sources":["../../src/router/directCloudRouter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAwB,cAAc,EAAsB,MAAM,0BAA0B,CAAC;AAEpG,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,KAAK,GAAG,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,mBAAmB,GAC3B,iBAAiB,GAAG,IAAI,CAgD1B;AA4CD;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,iBAAiB,EACzB,UAAU,CAAC,EAAE,UAAU,GACtB,OAAO,CAAC,cAAc,CAAC,CA+FzB"}
1
+ {"version":3,"file":"directCloudRouter.d.ts","sourceRoot":"","sources":["../../src/router/directCloudRouter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAwB,cAAc,EAAsB,MAAM,0BAA0B,CAAC;AAEpG,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,KAAK,GAAG,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,mBAAmB,GAC3B,iBAAiB,GAAG,IAAI,CAS1B;AA4CD;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,iBAAiB,EACzB,UAAU,CAAC,EAAE,UAAU,GACtB,OAAO,CAAC,cAAc,CAAC,CA+FzB"}
@@ -14,47 +14,14 @@ const logger_js_1 = require("../lib/logger.js");
14
14
  * Convert validated auth config to direct cloud config
15
15
  */
16
16
  function createDirectCloudConfig(routingDecision, headers) {
17
- const validatedConfig = routingDecision.validationResult?.config;
18
- if (!validatedConfig) {
19
- logger_js_1.logger.warn("No validated config available for direct cloud routing", {
20
- type: "DIRECT_CLOUD_CONFIG_ERROR",
21
- strategy: routingDecision.strategy,
22
- });
23
- return null;
24
- }
25
- // Extract SAP URL
26
- let sapUrl = validatedConfig.sapUrl;
27
- const destination = routingDecision.btpDestination || routingDecision.mcpDestination;
28
- if (!sapUrl && destination) {
29
- // For destination-based auth, URL comes from destination service key
30
- // We'll need to load it from AuthBroker
31
- // For now, return null and let the caller handle it
32
- logger_js_1.logger.debug("SAP URL not in validated config, will load from destination", {
33
- destination: destination,
34
- });
35
- }
36
- // Convert xsuaa to jwt (they're the same for connection purposes)
37
- const authType = validatedConfig.authType === "xsuaa" ? "jwt" : validatedConfig.authType;
38
- const config = {
39
- sapUrl: sapUrl || "", // Will be loaded from destination if needed
40
- destination: validatedConfig.destination || destination,
41
- authType: authType,
42
- sapClient: validatedConfig.sapClient,
43
- };
44
- // Add JWT-specific fields
45
- if (validatedConfig.authType === "jwt" || validatedConfig.authType === "xsuaa") {
46
- config.jwtToken = validatedConfig.jwtToken;
47
- config.refreshToken = validatedConfig.refreshToken;
48
- config.uaaUrl = validatedConfig.uaaUrl;
49
- config.uaaClientId = validatedConfig.uaaClientId;
50
- config.uaaClientSecret = validatedConfig.uaaClientSecret;
51
- }
52
- // Add basic auth fields
53
- if (validatedConfig.authType === "basic") {
54
- config.username = validatedConfig.username;
55
- config.password = validatedConfig.password;
56
- }
57
- return config;
17
+ // Proxy validates only x-btp-destination and x-mcp-destination headers
18
+ // This function is not used in the current proxy implementation
19
+ // as proxy passes all other headers directly to MCP server
20
+ logger_js_1.logger.warn("Direct cloud routing is not supported in proxy mode", {
21
+ type: "DIRECT_CLOUD_CONFIG_ERROR",
22
+ strategy: routingDecision.strategy,
23
+ });
24
+ return null;
58
25
  }
59
26
  /**
60
27
  * Connection cache for direct cloud connections
@@ -19,22 +19,27 @@ export interface RoutingDecision {
19
19
  }
20
20
  /**
21
21
  * Analyze headers and extract routing information
22
- * Proxy needs either:
22
+ *
23
+ * Proxy validates only two headers:
23
24
  * - x-btp-destination (or --btp): destination for BTP Cloud authorization token (Authorization: Bearer) and MCP server URL
24
- * - x-mcp-destination (or --mcp): destination for SAP ABAP connection (SAP headers) - can be used without BTP for local testing
25
+ * - x-mcp-destination (or --mcp): destination for SAP ABAP connection (SAP headers)
25
26
  *
26
27
  * Also supports:
27
- * - x-mcp-url (or --mcp-url): direct MCP server URL (used when no BTP destination is provided, for local testing)
28
+ * - x-mcp-url (or --mcp-url): direct MCP server URL (optional, for local testing)
28
29
  *
29
- * Command-line overrides:
30
+ * Command-line overrides (take precedence over headers):
30
31
  * - --btp=<destination>: overrides x-btp-destination header
31
32
  * - --mcp=<destination>: overrides x-mcp-destination header
32
- * - --mcp-url=<url>: overrides x-mcp-url header (for local testing without BTP)
33
+ * - --mcp-url=<url>: overrides x-mcp-url header
34
+ *
35
+ * Other headers (x-sap-url, x-sap-jwt-token, etc.) are passed directly to MCP server without validation.
33
36
  *
34
37
  * MCP server URL is obtained from:
35
38
  * - x-mcp-url header or --mcp-url parameter (if provided) - takes precedence
36
39
  * - service key for x-btp-destination (via auth-broker) if BTP destination is present
37
40
  * - service key for x-mcp-destination (via auth-broker) if only MCP destination is present
41
+ *
42
+ * Note: Proxy does NOT use .env files for connection configuration. Only destinations via auth-broker (service key files) are used.
38
43
  */
39
44
  export declare function analyzeHeaders(headers: IncomingHttpHeaders, configOverrides?: {
40
45
  btpDestination?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"headerAnalyzer.d.ts","sourceRoot":"","sources":["../../src/router/headerAnalyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAA2C,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAGjH,oBAAY,eAAe;IACzB,4CAA4C;IAC5C,KAAK,UAAU;IACf,6CAA6C;IAC7C,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,eAAe,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,mBAAmB,EAC5B,eAAe,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACtF,eAAe,CAyFjB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAGjE"}
1
+ {"version":3,"file":"headerAnalyzer.d.ts","sourceRoot":"","sources":["../../src/router/headerAnalyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAA2C,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAGjH,oBAAY,eAAe;IACzB,4CAA4C;IAC5C,KAAK,UAAU;IACf,6CAA6C;IAC7C,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,eAAe,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,mBAAmB,EAC5B,eAAe,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACtF,eAAe,CAqFjB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAGjE"}
@@ -17,22 +17,27 @@ var RoutingStrategy;
17
17
  })(RoutingStrategy || (exports.RoutingStrategy = RoutingStrategy = {}));
18
18
  /**
19
19
  * Analyze headers and extract routing information
20
- * Proxy needs either:
20
+ *
21
+ * Proxy validates only two headers:
21
22
  * - x-btp-destination (or --btp): destination for BTP Cloud authorization token (Authorization: Bearer) and MCP server URL
22
- * - x-mcp-destination (or --mcp): destination for SAP ABAP connection (SAP headers) - can be used without BTP for local testing
23
+ * - x-mcp-destination (or --mcp): destination for SAP ABAP connection (SAP headers)
23
24
  *
24
25
  * Also supports:
25
- * - x-mcp-url (or --mcp-url): direct MCP server URL (used when no BTP destination is provided, for local testing)
26
+ * - x-mcp-url (or --mcp-url): direct MCP server URL (optional, for local testing)
26
27
  *
27
- * Command-line overrides:
28
+ * Command-line overrides (take precedence over headers):
28
29
  * - --btp=<destination>: overrides x-btp-destination header
29
30
  * - --mcp=<destination>: overrides x-mcp-destination header
30
- * - --mcp-url=<url>: overrides x-mcp-url header (for local testing without BTP)
31
+ * - --mcp-url=<url>: overrides x-mcp-url header
32
+ *
33
+ * Other headers (x-sap-url, x-sap-jwt-token, etc.) are passed directly to MCP server without validation.
31
34
  *
32
35
  * MCP server URL is obtained from:
33
36
  * - x-mcp-url header or --mcp-url parameter (if provided) - takes precedence
34
37
  * - service key for x-btp-destination (via auth-broker) if BTP destination is present
35
38
  * - service key for x-mcp-destination (via auth-broker) if only MCP destination is present
39
+ *
40
+ * Note: Proxy does NOT use .env files for connection configuration. Only destinations via auth-broker (service key files) are used.
36
41
  */
37
42
  function analyzeHeaders(headers, configOverrides) {
38
43
  // Validate headers using header-validator
@@ -65,13 +70,11 @@ function analyzeHeaders(headers, configOverrides) {
65
70
  const extractedMcpUrl = configOverrides?.mcpUrl
66
71
  ? configOverrides.mcpUrl
67
72
  : mcpUrlHeader;
68
- // Allow proxying if either:
69
- // 1. BTP destination is present, OR
70
- // 2. MCP destination is present, OR
71
- // 3. MCP URL is provided (for local testing without BTP)
72
- // This enables local testing without BTP authentication
73
+ // Validate: at least one destination or MCP URL must be provided
74
+ // We validate only x-btp-destination and x-mcp-destination headers
75
+ // Other headers (x-sap-url, x-sap-jwt-token, etc.) are passed directly to MCP server
73
76
  if (!extractedBtpDestination && !extractedMcpDestination && !extractedMcpUrl) {
74
- logger_js_1.logger.warn("Neither x-btp-destination/x-mcp-destination/x-mcp-url header nor --btp/--mcp/--mcp-url parameter is provided", {
77
+ logger_js_1.logger.warn("Neither x-btp-destination/x-mcp-destination header nor --btp/--mcp parameter is provided, and no x-mcp-url/--mcp-url", {
75
78
  headers: Object.keys(headers).filter(k => k.toLowerCase().startsWith("x-")),
76
79
  hasBtpConfigOverride: !!configOverrides?.btpDestination,
77
80
  hasMcpConfigOverride: !!configOverrides?.mcpDestination,
@@ -83,11 +86,9 @@ function analyzeHeaders(headers, configOverrides) {
83
86
  validationResult,
84
87
  };
85
88
  }
86
- // If only MCP URL is provided without destination, we still need either mcpDestination or btpDestination
87
- // unless mcpUrl is a full URL that can be used directly
89
+ // If only MCP URL is provided without destinations, that's OK - other headers will be passed directly
88
90
  if (!extractedBtpDestination && !extractedMcpDestination && extractedMcpUrl) {
89
- // This is OK - we can use mcpUrl directly for local testing
90
- logger_js_1.logger.debug("Using MCP URL directly for local testing (no destination required)", {
91
+ logger_js_1.logger.debug("Using MCP URL directly (no destinations - headers will be passed as-is)", {
91
92
  type: "MCP_URL_DIRECT",
92
93
  mcpUrl: extractedMcpUrl,
93
94
  });
@@ -1 +1 @@
1
- {"version":3,"file":"localBasicRouter.d.ts","sourceRoot":"","sources":["../../src/router/localBasicRouter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAwB,cAAc,EAAsB,MAAM,0BAA0B,CAAC;AAEpG,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,mBAAmB,GAC3B,gBAAgB,GAAG,IAAI,CAkCzB;AA2CD;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,cAAc,CAAC,CAoEzB"}
1
+ {"version":3,"file":"localBasicRouter.d.ts","sourceRoot":"","sources":["../../src/router/localBasicRouter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAwB,cAAc,EAAsB,MAAM,0BAA0B,CAAC;AAEpG,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,mBAAmB,GAC3B,gBAAgB,GAAG,IAAI,CASzB;AA2CD;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,cAAc,CAAC,CAoEzB"}
@@ -13,36 +13,14 @@ const logger_js_1 = require("../lib/logger.js");
13
13
  * Convert validated auth config to local basic config
14
14
  */
15
15
  function createLocalBasicConfig(routingDecision, headers) {
16
- const validatedConfig = routingDecision.validationResult?.config;
17
- if (!validatedConfig) {
18
- logger_js_1.logger.warn("No validated config available for local basic routing", {
19
- type: "LOCAL_BASIC_CONFIG_ERROR",
20
- strategy: routingDecision.strategy,
21
- });
22
- return null;
23
- }
24
- if (validatedConfig.authType !== "basic") {
25
- logger_js_1.logger.warn("Config is not basic auth type", {
26
- type: "LOCAL_BASIC_CONFIG_TYPE_ERROR",
27
- authType: validatedConfig.authType,
28
- });
29
- return null;
30
- }
31
- if (!validatedConfig.sapUrl || !validatedConfig.username || !validatedConfig.password) {
32
- logger_js_1.logger.warn("Missing required fields for basic auth", {
33
- type: "LOCAL_BASIC_CONFIG_MISSING",
34
- hasUrl: !!validatedConfig.sapUrl,
35
- hasUsername: !!validatedConfig.username,
36
- hasPassword: !!validatedConfig.password,
37
- });
38
- return null;
39
- }
40
- return {
41
- sapUrl: validatedConfig.sapUrl,
42
- username: validatedConfig.username,
43
- password: validatedConfig.password,
44
- sapClient: validatedConfig.sapClient,
45
- };
16
+ // Proxy validates only x-btp-destination and x-mcp-destination headers
17
+ // This function is not used in the current proxy implementation
18
+ // as proxy passes all other headers directly to MCP server
19
+ logger_js_1.logger.warn("Local basic routing is not supported in proxy mode", {
20
+ type: "LOCAL_BASIC_CONFIG_ERROR",
21
+ strategy: routingDecision.strategy,
22
+ });
23
+ return null;
46
24
  }
47
25
  /**
48
26
  * Connection cache for local basic connections
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/proxy",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -38,20 +38,21 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@modelcontextprotocol/inspector": "^0.17.2",
41
- "@types/jest": "^29.5.0",
41
+ "@types/jest": "^29.5.14",
42
42
  "@types/node": "^24.2.1",
43
43
  "commander": "^14.0.2",
44
44
  "cors": "^2.8.5",
45
- "dotenv": "^17.2.1",
46
45
  "express": "^5.1.0",
47
46
  "jest": "^29.7.0",
48
47
  "ts-jest": "^29.2.0",
49
48
  "typescript": "^5.9.2"
50
49
  },
51
50
  "dependencies": {
52
- "@mcp-abap-adt/auth-broker": "^0.1.5",
53
- "@mcp-abap-adt/connection": "^0.1.13",
54
- "@mcp-abap-adt/header-validator": "^0.1.3",
51
+ "@mcp-abap-adt/auth-broker": "^0.1.9",
52
+ "@mcp-abap-adt/auth-providers": "^0.1.2",
53
+ "@mcp-abap-adt/auth-stores": "^0.1.4",
54
+ "@mcp-abap-adt/connection": "^0.1.15",
55
+ "@mcp-abap-adt/header-validator": "^0.1.6",
55
56
  "@modelcontextprotocol/sdk": "^1.23.0",
56
57
  "axios": "^1.11.0",
57
58
  "zod": "^3.25.76"