@mcp-abap-adt/proxy 0.1.9 → 1.0.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,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.0] - 2026-04-07
11
+
12
+ ### Breaking Changes
13
+ - **Header renamed: `x-btp-destination` → `x-sap-destination`**
14
+ - The proxy now uses `x-sap-destination` header (same as MCP server) instead of the proxy-specific `x-btp-destination`
15
+ - The `--btp` CLI parameter is unchanged
16
+ - Clients sending `x-btp-destination` header must update to `x-sap-destination`
17
+
18
+ ### Added
19
+ - **Transparent reverse proxy for BTP request forwarding**
20
+ - Replaced buffered axios-based proxy with Node.js native HTTP streaming reverse proxy
21
+ - Supports any HTTP API including OpenAI-compatible streaming endpoints
22
+ - `targetUrl` parameter for explicit target URL override (`--target-url`/`--url` or `x-target-url` header)
23
+ - **Interactive configuration wizard (TUI)**
24
+ - `npx @mcp-abap-adt/proxy --tui` launches interactive setup wizard
25
+ - Service key existence checker validates BTP destination before configuration
26
+ - Generates YAML config files with inline comments
27
+ - Supports BTP and Direct URL scenarios with advanced settings
28
+ - **Dependencies updated**
29
+ - `@mcp-abap-adt/interfaces` bumped to v5
30
+ - `@mcp-abap-adt/auth-stores` bumped to v1.0.4
31
+ - Added `@inquirer/prompts` for TUI wizard
32
+
33
+ ### Fixed
34
+ - **Header validation skipped in reverse proxy mode** when config provides destination
35
+ - **YAML config file now correctly loads transport/port settings** (`--config` flag)
36
+ - **targetUrl loading from YAML config** now works correctly
37
+
10
38
  ## [0.1.9] - 2026-02-12
11
39
 
12
40
  ### Changed
@@ -45,7 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
45
73
 
46
74
  ### Added
47
75
  - **Separate AuthBroker instances for BTP and ABAP destinations**
48
- - `btpAuthBroker` with `XsuaaTokenProvider` for BTP destinations (`--btp` or `x-btp-destination`)
76
+ - `btpAuthBroker` with `XsuaaTokenProvider` for BTP destinations (`--btp` or `x-sap-destination`)
49
77
  - `abapAuthBroker` with `BtpTokenProvider` for ABAP destinations (`--mcp` or `x-mcp-destination`)
50
78
  - Proper separation ensures correct token provider is used for each destination type
51
79
  - **Client Setup Guide** (`docs/CLIENT_SETUP.md`)
@@ -89,7 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
89
117
  - **Documentation for BTP-Only authentication mode**
90
118
  - Added "Scenario 2: BTP Authentication Only (No SAP Configuration)" in `docs/USAGE.md`
91
119
  - Documented use case for connecting to any BTP service without SAP ABAP configuration
92
- - Explained that `x-mcp-destination` is optional when using `x-btp-destination` or `--btp`
120
+ - Explained that `x-mcp-destination` is optional when using `x-sap-destination` or `--btp`
93
121
  - Updated README.md with "BTP-Only Mode" section explaining behavior when only `--btp` is provided
94
122
  - **Enhanced help text in `bin/mcp-abap-adt-proxy.js`**
95
123
  - Added detailed "Usage Modes" section with three modes:
@@ -111,7 +139,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
111
139
  ### Fixed
112
140
  - **Unit tests updated to match new routing logic**
113
141
  - Fixed tests that expected `UNKNOWN` strategy when only `x-mcp-destination` or `x-mcp-url` is provided
114
- - Updated test expectations to reflect that `x-btp-destination` is now optional
142
+ - Updated test expectations to reflect that `x-sap-destination` is now optional
115
143
  - Added tests for `x-mcp-url` header support
116
144
  - Updated `shouldProxy` function tests to cover all routing scenarios (BTP, MCP destination, MCP URL)
117
145
  - All 51 unit tests now pass successfully
@@ -134,7 +162,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
134
162
 
135
163
  ### Added
136
164
  - **Local testing mode without BTP authentication**
137
- - Proxy can now run with only `--mcp` or `x-mcp-destination` (without `--btp` or `x-btp-destination`)
165
+ - Proxy can now run with only `--mcp` or `x-mcp-destination` (without `--btp` or `x-sap-destination`)
138
166
  - Enables local integration testing without BTP authentication
139
167
  - MCP server URL can be obtained from MCP destination service key or specified directly via `--mcp-url`
140
168
  - **`--mcp-url` parameter support**
@@ -149,16 +177,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
149
177
  - Default configuration: ADT on port 3000 (HTTP), Proxy on port 3001 (HTTP)
150
178
  - Available via `npm run test:servers`
151
179
  - **Flexible routing requirements**
152
- - Proxy accepts either `x-btp-destination/--btp`, `x-mcp-destination/--mcp`, or `x-mcp-url/--mcp-url`
180
+ - Proxy accepts either `x-sap-destination/--btp`, `x-mcp-destination/--mcp`, or `x-mcp-url/--mcp-url`
153
181
  - Removed strict requirement for BTP destination when using MCP destination or URL
154
182
 
155
183
  ### Changed
156
184
  - **Routing logic updated**
157
- - `x-btp-destination` is now optional (was required)
185
+ - `x-sap-destination` is now optional (was required)
158
186
  - Proxy can work with only `x-mcp-destination` or `x-mcp-url` for local testing
159
187
  - MCP URL can be obtained from multiple sources (priority order):
160
188
  1. `x-mcp-url` header or `--mcp-url` parameter (direct URL)
161
- 2. Service key for `x-btp-destination` (if provided)
189
+ 2. Service key for `x-sap-destination` (if provided)
162
190
  3. Service key for `x-mcp-destination` (if only MCP destination is provided)
163
191
  - **Token retrieval made optional for MCP destination**
164
192
  - Token retrieval for `mcpDestination` is now optional (won't fail if token unavailable)
package/README.md CHANGED
@@ -76,7 +76,7 @@ For detailed setup instructions for Cline and GitHub Copilot, see the **[Client
76
76
  "type": "streamableHttp",
77
77
  "url": "http://localhost:3001/mcp/stream/http",
78
78
  "headers": {
79
- "x-btp-destination": "btp-cloud"
79
+ "x-sap-destination": "btp-cloud"
80
80
  }
81
81
  }
82
82
  }
@@ -84,10 +84,10 @@ For detailed setup instructions for Cline and GitHub Copilot, see the **[Client
84
84
  ```
85
85
 
86
86
  **Required Headers:**
87
- - `x-btp-destination` - Destination name for BTP Cloud authorization token and MCP server URL
87
+ - `x-sap-destination` - Destination name for BTP Cloud authorization token and MCP server URL
88
88
 
89
89
  **Command Line Overrides:**
90
- - `--btp=<destination>` - Overrides `x-btp-destination` header (takes precedence)
90
+ - `--btp=<destination>` - Overrides `x-sap-destination` header (takes precedence)
91
91
  - `--url=<url>` - Overrides MCP server URL (required if service key lacks URL)
92
92
  - `--browser=<browser>` - Browser to use: `system` (default), `chrome`, `edge`, `firefox`, `headless`
93
93
  - `--browser-auth-port=<port>` - Port for OAuth2 callback (default: 3333)
@@ -97,7 +97,7 @@ For detailed setup instructions for Cline and GitHub Copilot, see the **[Client
97
97
 
98
98
  The proxy uses BTP/XSUAA authentication:
99
99
 
100
- 1. **BTP Authentication** (if `--btp` or `x-btp-destination` is present):
100
+ 1. **BTP Authentication** (if `--btp` or `x-sap-destination` is present):
101
101
  - Uses `AuthorizationCodeProvider` (browser-based OAuth2 flow)
102
102
  - **Eager Authentication**: Opens browser immediately on startup to get token
103
103
  - Injects/overwrites `Authorization: Bearer <token>` header
@@ -106,7 +106,7 @@ The proxy uses BTP/XSUAA authentication:
106
106
 
107
107
  **BTP Authentication Mode** (with `--btp`):
108
108
  1. Proxy starts → Opens browser for login (Eager Auth) → Gets/Refreshes JWT token
109
- 2. `x-btp-destination` (or `--btp`) → Adds `Authorization: Bearer <token>` header
109
+ 2. `x-sap-destination` (or `--btp`) → Adds `Authorization: Bearer <token>` header
110
110
  3. MCP server URL obtained from service key OR `--url` parameter
111
111
 
112
112
  ## Documentation
@@ -126,16 +126,16 @@ The proxy uses BTP/XSUAA authentication:
126
126
 
127
127
  The proxy performs the following steps for each request:
128
128
 
129
- 1. **Extract Headers**: Reads `x-btp-destination` header
129
+ 1. **Extract Headers**: Reads `x-sap-destination` header
130
130
  2. **Apply Command Line Overrides**: `--btp` parameter overrides header (if provided)
131
- 3. **Validate Routing Requirements**: Requires `x-btp-destination/--btp`
132
- 4. **BTP Authentication** (if `x-btp-destination` or `--btp` is provided):
131
+ 3. **Validate Routing Requirements**: Requires `x-sap-destination/--btp`
132
+ 4. **BTP Authentication** (if `x-sap-destination` or `--btp` is provided):
133
133
  - Uses `AuthorizationCodeProvider` (browser-based login)
134
134
  - **Eagerly** obtains token on startup (if configured via `--btp`)
135
135
  - Retrieves JWT token using cached refresh token or opens browser
136
136
  - Injects/overwrites `Authorization: Bearer <token>` header
137
137
  5. **Get MCP Server URL**:
138
- - From service key for `x-btp-destination`
138
+ - From service key for `x-sap-destination`
139
139
  6. **Forward Request**: Sends request to MCP server URL with all injected headers
140
140
  7. **Return Response**: Forwards the response back to the client
141
141
 
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * Usage:
9
9
  * mcp-abap-adt-proxy [options]
10
+ * mcp-abap-adt-proxy tui
10
11
  * mcp-abap-adt-proxy --transport=stdio
11
12
  * mcp-abap-adt-proxy --transport=http
12
13
  */
@@ -43,6 +44,10 @@ MCP ABAP ADT Proxy Server v${pkg.version}
43
44
 
44
45
  Usage:
45
46
  mcp-abap-adt-proxy [options]
47
+ mcp-abap-adt-proxy tui
48
+
49
+ Commands:
50
+ tui Interactive configuration wizard (generates YAML config)
46
51
 
47
52
  Options:
48
53
  --transport=<type> Transport type: stdio, http, streamable-http, sse
@@ -50,7 +55,7 @@ Options:
50
55
  --sse-port=<port> SSE server port (default: 3002)
51
56
  --http-host=<host> HTTP server host (default: 0.0.0.0)
52
57
  --sse-host=<host> SSE server host (default: 0.0.0.0)
53
- --btp=<destination> Override x-btp-destination header (for BTP Cloud authorization)
58
+ --btp=<destination> Override x-sap-destination header (for BTP Cloud authorization)
54
59
  Required for stdio/SSE transports unless provided in headers
55
60
  --target-url=<url> Override target URL (separates Auth from Connection)
56
61
  Useful for direct OData service testing or non-standard MCP URLs
@@ -77,6 +82,7 @@ Examples:
77
82
  mcp-abap-adt-proxy --transport=sse --sse-port=3002 # SSE mode on port 3002
78
83
  mcp-abap-adt-proxy --btp=ai # With BTP destination override
79
84
  mcp-abap-adt-proxy --btp=ai --unsafe # With file-based session storage
85
+ mcp-abap-adt-proxy tui # Interactive configuration wizard
80
86
  mcp-abap-adt-proxy --config=proxy-config.yaml # Load configuration from YAML file
81
87
  mcp-abap-adt-proxy -c proxy-config.yml # Load configuration from YAML file (short form)
82
88
 
@@ -94,7 +100,7 @@ Usage Modes:
94
100
  Example: mcp-abap-adt-proxy --btp=ai
95
101
 
96
102
  Headers (for HTTP/SSE transports):
97
- - x-btp-destination: Destination for BTP Cloud authorization (optional if --btp is used)
103
+ - x-sap-destination: Destination for BTP Cloud authorization (optional if --btp is used)
98
104
 
99
105
  At least one of the above headers (or corresponding --btp parameter) is required.
100
106
 
@@ -137,6 +143,30 @@ function main() {
137
143
  process.exit(0);
138
144
  }
139
145
 
146
+ // Handle 'tui' subcommand — interactive configuration wizard
147
+ if (process.argv[2] === 'tui') {
148
+ const wizardPath = path.resolve(__dirname, '../dist/tui/index.js');
149
+ if (!fs.existsSync(wizardPath)) {
150
+ process.stderr.write(`[MCP Proxy] ✗ Wizard not found at: ${wizardPath}\n`);
151
+ process.stderr.write(`[MCP Proxy] Make sure to build the project with 'npm run build' first.\n`);
152
+ process.exit(1);
153
+ return;
154
+ }
155
+ import(wizardPath).then(({ runWizard }) => {
156
+ runWizard().catch((err) => {
157
+ if (err.name === 'ExitPromptError') {
158
+ process.exit(0);
159
+ }
160
+ process.stderr.write(`[MCP Proxy] ✗ Wizard error: ${err.message}\n`);
161
+ process.exit(1);
162
+ });
163
+ }).catch((err) => {
164
+ process.stderr.write(`[MCP Proxy] ✗ Failed to load wizard: ${err.message}\n`);
165
+ process.exit(1);
166
+ });
167
+ return;
168
+ }
169
+
140
170
  // Spawn the main server process
141
171
  const serverPath = path.resolve(__dirname, '../dist/index.js');
142
172
 
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export declare class McpAbapAdtProxyServer {
31
31
  */
32
32
  private startHttpServer;
33
33
  /**
34
- * Handle proxy request - add JWT token and forward to MCP server
34
+ * Handle proxy request - get JWT token and forward transparently via reverse proxy
35
35
  */
36
36
  private handleProxyRequest;
37
37
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiBH,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAsBlC;;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;IA0ClE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAuE1B;;OAEG;YACW,eAAe;IA4M7B;;OAEG;YACW,kBAAkB;IAyOhC;;OAEG;YACW,cAAc;IA0S5B;;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;IA2T5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAmBhC;AAGD,eAAe,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ const interfaces_1 = require("@mcp-abap-adt/interfaces");
15
15
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
16
16
  const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
17
17
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
18
+ const reverseProxy_js_1 = require("./proxy/reverseProxy.js");
18
19
  const config_js_1 = require("./lib/config.js");
19
20
  const logger_js_1 = require("./lib/logger.js");
20
21
  const transportConfig_js_1 = require("./lib/transportConfig.js");
@@ -40,7 +41,17 @@ class McpAbapAdtProxyServer {
40
41
  httpServer;
41
42
  btpProxy;
42
43
  constructor(transportConfig, configPath) {
43
- this.transportConfig = transportConfig || (0, transportConfig_js_1.parseTransportConfig)();
44
+ // Load config first to extract transport overrides from YAML/JSON file
45
+ const resolvedConfigPath = configPath || (0, config_js_1.getConfigPath)();
46
+ let fileTransportOverrides;
47
+ if (resolvedConfigPath) {
48
+ const raw = (0, config_js_1.loadRawConfigFile)(resolvedConfigPath);
49
+ if (raw) {
50
+ fileTransportOverrides = (0, config_js_1.extractTransportFields)(raw);
51
+ }
52
+ }
53
+ this.transportConfig =
54
+ transportConfig || (0, transportConfig_js_1.parseTransportConfig)(fileTransportOverrides);
44
55
  this.config = (0, config_js_1.loadConfig)(configPath);
45
56
  // Validate configuration
46
57
  if (this.transportConfig.type === 'streamable-http' ||
@@ -145,9 +156,7 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
145
156
  async startHttpServer() {
146
157
  const httpConfig = this.transportConfig;
147
158
  this.httpServer = (0, node_http_1.createServer)(async (req, res) => {
148
- // Log incoming HTTP request (always log in DEBUG mode)
149
- const _debugEnabled = process.env.DEBUG === 'true' ||
150
- process.env.DEBUG_HTTP_REQUESTS === 'true';
159
+ // Log incoming HTTP request
151
160
  const sanitizedHeaders = (0, requestInterceptor_js_1.sanitizeHeadersForLogging)(req.headers);
152
161
  logger_js_1.logger?.info('=== HTTP REQUEST RECEIVED ===', {
153
162
  type: 'HTTP_REQUEST_RECEIVED',
@@ -156,92 +165,14 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
156
165
  headers: sanitizedHeaders,
157
166
  remoteAddress: req.socket.remoteAddress,
158
167
  });
159
- // Allow all HTTP methods (GET, POST, PUT, DELETE, etc.)
160
- // if (req.method !== 'POST') {
161
- // logger?.warn('Non-POST request rejected', { ... });
162
- // ...
163
- // }
164
- // Read request body
165
- let body;
166
- try {
167
- const chunks = [];
168
- for await (const chunk of req) {
169
- chunks.push(chunk);
170
- }
171
- const bodyString = Buffer.concat(chunks).toString('utf-8');
172
- logger_js_1.logger?.info('=== PARSING REQUEST BODY ===', {
173
- type: 'REQUEST_BODY_PARSING',
174
- bodyLength: bodyString.length,
175
- bodyPreview: bodyString.substring(0, 200),
176
- });
177
- if (bodyString.trim().length > 0) {
178
- body = JSON.parse(bodyString);
179
- }
180
- else {
181
- body = undefined;
182
- }
183
- // Sanitize body for logging
184
- const sanitizedBody = {};
185
- if (body && typeof body === 'object' && body !== null) {
186
- const bodyObj = body;
187
- sanitizedBody.method = bodyObj.method;
188
- sanitizedBody.id = bodyObj.id;
189
- sanitizedBody.jsonrpc = bodyObj.jsonrpc;
190
- if (bodyObj.params &&
191
- typeof bodyObj.params === 'object' &&
192
- bodyObj.params !== null) {
193
- const params = bodyObj.params;
194
- sanitizedBody.params = {};
195
- const sanitizedParams = sanitizedBody.params;
196
- if (params.arguments &&
197
- typeof params.arguments === 'object' &&
198
- params.arguments !== null) {
199
- sanitizedParams.arguments = {};
200
- const sanitizedArgs = sanitizedParams.arguments;
201
- for (const [key, value] of Object.entries(params.arguments)) {
202
- const lowerKey = key.toLowerCase();
203
- if (lowerKey.includes('password') ||
204
- lowerKey.includes('token') ||
205
- lowerKey.includes('secret')) {
206
- sanitizedArgs[key] = '[REDACTED]';
207
- }
208
- else {
209
- sanitizedArgs[key] = value;
210
- }
211
- }
212
- }
213
- for (const [key, value] of Object.entries(params)) {
214
- if (key === 'arguments')
215
- continue;
216
- sanitizedParams[key] = value;
217
- }
218
- }
219
- }
220
- logger_js_1.logger?.info('=== REQUEST BODY PARSED ===', {
221
- type: 'REQUEST_BODY_PARSED',
222
- body: sanitizedBody,
223
- });
224
- }
225
- catch (error) {
226
- logger_js_1.logger?.error('=== FAILED TO PARSE REQUEST BODY ===', {
227
- type: 'REQUEST_PARSE_ERROR',
228
- error: error instanceof Error ? error.message : String(error),
229
- errorStack: error instanceof Error ? error.stack : undefined,
230
- });
231
- res.writeHead(400, { 'Content-Type': 'text/plain' });
232
- res.end('Invalid request body');
233
- return;
234
- }
235
- // Intercept and analyze request
168
+ // Intercept and analyze request (headers only, body is piped through)
236
169
  const configOverrides = {
237
170
  btpDestination: this.config.btpDestination,
238
171
  targetUrl: this.config.targetUrl,
239
172
  };
240
- logger_js_1.logger?.info('=== INTERCEPTING REQUEST ===', {
241
- type: 'REQUEST_INTERCEPTING',
242
- configOverrides,
173
+ const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, undefined, configOverrides, {
174
+ skipHeaderValidation: true,
243
175
  });
244
- const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
245
176
  // Check routing decision
246
177
  if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
247
178
  logger_js_1.logger?.error('Routing decision failed', {
@@ -249,25 +180,16 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
249
180
  reason: intercepted.routingDecision.reason,
250
181
  });
251
182
  res.writeHead(400, { 'Content-Type': 'application/json' });
252
- const bodyId = body && typeof body === 'object' && body !== null && 'id' in body
253
- ? body.id
254
- : null;
255
183
  res.end(JSON.stringify({
256
- jsonrpc: '2.0',
257
- id: bodyId || null,
258
- error: {
259
- code: -32602,
260
- message: intercepted.routingDecision.reason,
261
- },
184
+ error: intercepted.routingDecision.reason,
262
185
  }));
263
186
  return;
264
187
  }
265
- // Log proxy request
188
+ // Forward request via reverse proxy
266
189
  logger_js_1.logger?.info('=== PROXYING REQUEST ===', {
267
190
  type: 'PROXY_REQUEST_START',
268
191
  btpDestination: intercepted.routingDecision.btpDestination,
269
192
  });
270
- // Handle proxy request - add JWT and forward to x-mcp-url
271
193
  try {
272
194
  await this.handleProxyRequest(intercepted, req, res);
273
195
  }
@@ -278,8 +200,8 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
278
200
  strategy: intercepted.routingDecision.strategy,
279
201
  });
280
202
  if (!res.headersSent) {
281
- res.writeHead(500, { 'Content-Type': 'text/plain' });
282
- res.end('Internal server error');
203
+ res.writeHead(500, { 'Content-Type': 'application/json' });
204
+ res.end(JSON.stringify({ error: 'Internal server error' }));
283
205
  }
284
206
  }
285
207
  });
@@ -311,192 +233,38 @@ Authorization source: ${authSourceObj}`;
311
233
  });
312
234
  }
313
235
  /**
314
- * Handle proxy request - add JWT token and forward to MCP server
236
+ * Handle proxy request - get JWT token and forward transparently via reverse proxy
315
237
  */
316
238
  async handleProxyRequest(intercepted, req, res) {
317
- const btpDestination = intercepted.routingDecision.btpDestination;
318
- // Log incoming request details
319
- const incomingHeaders = {};
320
- for (const [key, value] of Object.entries(intercepted.headers)) {
321
- const lowerKey = key.toLowerCase();
322
- if (lowerKey.includes('token') ||
323
- lowerKey.includes('authorization') ||
324
- lowerKey.includes('password') ||
325
- lowerKey.includes('secret')) {
326
- incomingHeaders[key] = '[REDACTED]';
327
- }
328
- else {
329
- incomingHeaders[key] = Array.isArray(value)
330
- ? value.join(', ')
331
- : value || '';
332
- }
239
+ const destination = intercepted.routingDecision.btpDestination;
240
+ if (!destination) {
241
+ res.writeHead(400, { 'Content-Type': 'application/json' });
242
+ res.end(JSON.stringify({ error: 'No BTP destination specified' }));
243
+ return;
333
244
  }
334
- const sanitizedBody = {};
335
- if (intercepted.body &&
336
- typeof intercepted.body === 'object' &&
337
- intercepted.body !== null) {
338
- const bodyObj = intercepted.body;
339
- if (bodyObj.params &&
340
- typeof bodyObj.params === 'object' &&
341
- bodyObj.params !== null) {
342
- const params = bodyObj.params;
343
- sanitizedBody.params = {};
344
- const sanitizedParams = sanitizedBody.params;
345
- // For tools/call, log arguments
346
- if (params.arguments &&
347
- typeof params.arguments === 'object' &&
348
- params.arguments !== null) {
349
- sanitizedParams.arguments = {};
350
- const sanitizedArgs = sanitizedParams.arguments;
351
- for (const [key, value] of Object.entries(params.arguments)) {
352
- const lowerKey = key.toLowerCase();
353
- if (lowerKey.includes('password') ||
354
- lowerKey.includes('token') ||
355
- lowerKey.includes('secret')) {
356
- sanitizedArgs[key] = '[REDACTED]';
357
- }
358
- else {
359
- sanitizedArgs[key] = value;
360
- }
361
- }
362
- }
363
- // Log other params
364
- for (const [key, value] of Object.entries(params)) {
365
- if (key === 'arguments')
366
- continue;
367
- sanitizedParams[key] = value;
368
- }
369
- }
370
- sanitizedBody.method = bodyObj.method;
371
- sanitizedBody.id = bodyObj.id;
372
- sanitizedBody.jsonrpc = bodyObj.jsonrpc;
245
+ // Ensure proxy is initialized
246
+ if (!this.btpProxy) {
247
+ this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
373
248
  }
374
- // Use original request body structure for proxying (generic support)
375
- // We do NOT enforce JSON-RPC structure here anymore, to support REST/OData
376
- const proxyRequestData = intercepted.body;
377
- logger_js_1.logger?.info('=== INCOMING REQUEST ===', {
378
- type: 'PROXY_REQUEST_INCOMING',
379
- method: req.method,
380
- url: req.url,
381
- btpDestination,
382
- sessionId: intercepted.sessionId,
383
- headers: incomingHeaders,
384
- body: sanitizedBody,
385
- mode: 'BTP authentication',
386
- });
387
249
  try {
388
- // Ensure proxy is initialized (URL will be obtained from service key for btpDestination or mcpDestination)
389
- if (!this.btpProxy) {
390
- this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
391
- }
392
- // Build MCP request from intercepted request
393
- // Note: Use ?? instead of || to preserve falsy values like 0
394
- const interceptedBodyId = getBodyId(intercepted.body);
395
- const interceptedBodyIdType = typeof interceptedBodyId;
396
- const interceptedBodyIdUndefined = interceptedBodyId === undefined;
397
- logger_js_1.logger?.info('=== BEFORE BUILDING MCP REQUEST ===', {
398
- type: 'BEFORE_BUILD_MCP_REQUEST',
399
- interceptedBodyId,
400
- interceptedBodyIdType,
401
- interceptedBodyIdUndefined,
402
- interceptedBodyIdNull: interceptedBodyId === null,
403
- interceptedBodyIdZero: interceptedBodyId === 0,
404
- interceptedBodyIdFalsy: !interceptedBodyId,
405
- fullInterceptedBody: JSON.stringify(intercepted.body),
406
- });
407
- // Preserve id correctly - 0 is a valid id value
408
- let mcpRequestId;
409
- if (interceptedBodyId === undefined) {
410
- mcpRequestId = null;
411
- }
412
- else if (typeof interceptedBodyId === 'string' ||
413
- typeof interceptedBodyId === 'number') {
414
- mcpRequestId = interceptedBodyId;
415
- }
416
- else {
417
- mcpRequestId = null;
418
- }
419
- // Proxy request to BtpProxy
420
- // Pass the original request data directly
421
- const proxyRequest = {
422
- method: intercepted.method, // Use original HTTP method (GET, POST, etc.)
423
- url: intercepted.url, // Pass request URL (path)
424
- data: proxyRequestData, // Use original body (or undefined)
425
- id: mcpRequestId, // Keep ID for logging context if available
426
- };
427
- const proxyResponse = await this.btpProxy.proxyRequest(proxyRequest, intercepted.routingDecision, intercepted.headers);
428
- // Fix: If backend returned null ID but we entered with a valid ID (e.g. 0), restore it.
429
- // Cline/Zod schemas reject null ID for successful responses if request had an ID.
430
- if ((proxyResponse.id === null || proxyResponse.id === undefined) &&
431
- mcpRequestId !== null &&
432
- mcpRequestId !== undefined) {
433
- logger_js_1.logger?.debug('Restoring original request ID in response', {
434
- originalId: mcpRequestId,
435
- receivedId: proxyResponse.id,
436
- });
437
- proxyResponse.id = mcpRequestId;
438
- }
439
- logger_js_1.logger?.info('=== FORWARDING REQUEST ===', {
440
- type: 'PROXY_REQUEST_FORWARDING',
441
- interceptedBodyIdAtForward: getBodyId(intercepted.body),
442
- proxyRequest,
443
- routingDecision: intercepted.routingDecision,
444
- headers: intercepted.headers,
445
- });
446
- // Log response
447
- const sanitizedResponse = {
448
- jsonrpc: proxyResponse.jsonrpc,
449
- id: proxyResponse.id,
450
- };
451
- if (proxyResponse.result) {
452
- sanitizedResponse.result = proxyResponse.result;
453
- }
454
- if (proxyResponse.error) {
455
- sanitizedResponse.error = {
456
- code: proxyResponse.error.code,
457
- message: proxyResponse.error.message,
458
- data: proxyResponse.error.data,
459
- };
460
- }
461
- logger_js_1.logger?.info('=== RESPONSE FROM TARGET SERVICE ===', {
462
- type: 'PROXY_RESPONSE_RECEIVED',
463
- response: sanitizedResponse,
464
- hasResult: !!proxyResponse.result,
465
- hasError: !!proxyResponse.error,
466
- });
467
- // Send response back to client
468
- res.writeHead(200, { 'Content-Type': 'application/json' });
469
- res.end(JSON.stringify(proxyResponse));
470
- logger_js_1.logger?.info('=== RESPONSE SENT TO CLIENT ===', {
471
- type: 'PROXY_RESPONSE_SENT',
472
- statusCode: 200,
473
- });
474
- logger_js_1.logger?.debug('BtpProxy request completed', {
475
- type: 'BTP_PROXY_COMPLETED',
476
- hasResult: !!proxyResponse.result,
477
- hasError: !!proxyResponse.error,
478
- });
250
+ // Get JWT token (cached, auto-refresh)
251
+ const jwtToken = await this.btpProxy.getJwtToken(destination);
252
+ // Get target URL
253
+ const targetUrl = intercepted.routingDecision.targetUrl ||
254
+ (await this.btpProxy.getTargetUrl(destination));
255
+ // Forward request transparently
256
+ await (0, reverseProxy_js_1.forwardRequest)(req, res, targetUrl, jwtToken);
479
257
  }
480
258
  catch (error) {
481
- const errorMessage = error instanceof Error ? error.message : String(error);
482
- logger_js_1.logger?.error('Failed to handle BtpProxy request', {
483
- type: 'BTP_PROXY_ERROR',
484
- error: errorMessage,
485
- stack: error instanceof Error ? error.stack : undefined,
259
+ logger_js_1.logger?.error('Proxy request failed', {
260
+ type: 'PROXY_REQUEST_ERROR',
261
+ destination,
262
+ error: error instanceof Error ? error.message : String(error),
486
263
  });
487
- // Output error to stderr for user visibility (only if verbose mode is enabled)
488
- if ((0, btpProxy_js_1.shouldWriteStderr)()) {
489
- process.stderr.write(`[MCP Proxy] ✗ Connection error: ${errorMessage}\n`);
490
- }
491
264
  if (!res.headersSent) {
492
- res.writeHead(500, { 'Content-Type': 'application/json' });
265
+ res.writeHead(502, { 'Content-Type': 'application/json' });
493
266
  res.end(JSON.stringify({
494
- jsonrpc: '2.0',
495
- id: getBodyId(intercepted.body) || null,
496
- error: {
497
- code: -32000,
498
- message: errorMessage,
499
- },
267
+ error: error instanceof Error ? error.message : 'Proxy error',
500
268
  }));
501
269
  }
502
270
  }
@@ -686,6 +454,7 @@ Authorization source: ${authSourceObj}`;
686
454
  // Intercept and analyze request with config overrides
687
455
  const configOverrides = {
688
456
  btpDestination: this.config.btpDestination,
457
+ targetUrl: this.config.targetUrl,
689
458
  };
690
459
  const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
691
460
  // Check routing decision
@@ -704,9 +473,19 @@ Authorization source: ${authSourceObj}`;
704
473
  }));
705
474
  return;
706
475
  }
707
- // Handle proxy request
476
+ // Handle proxy request via BtpProxy (JSON-RPC mode for SSE)
708
477
  try {
709
- await this.handleProxyRequest(intercepted, req, res);
478
+ if (!this.btpProxy) {
479
+ this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
480
+ }
481
+ const proxyResponse = await this.btpProxy.proxyRequest({
482
+ method: intercepted.method,
483
+ url: intercepted.url,
484
+ data: body,
485
+ id: getBodyId(body),
486
+ }, intercepted.routingDecision, intercepted.headers);
487
+ res.writeHead(200, { 'Content-Type': 'application/json' });
488
+ res.end(JSON.stringify(proxyResponse));
710
489
  }
711
490
  catch (error) {
712
491
  logger_js_1.logger?.error('Failed to process SSE POST request', {