@mcp-abap-adt/proxy 0.1.8 → 0.2.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,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.9] - 2026-02-12
11
+
12
+ ### Changed
13
+ - **AUTH_BROKER_PATH resolution aligned with `mcp-abap-adt`**
14
+ - `AUTH_BROKER_PATH` is now treated as a base directory.
15
+ - Proxy now resolves stores from `<base>/service-keys` and `<base>/sessions`.
16
+ - If a path already ends with `service-keys` or `sessions`, it is normalized and still resolved correctly.
17
+
18
+ ### Documentation
19
+ - Clarified in README and troubleshooting docs how default store paths and `AUTH_BROKER_PATH` are resolved.
20
+
10
21
  ## [0.1.8] - 2026-02-05
11
22
 
12
23
  ### Added
@@ -198,4 +209,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
198
209
  - `@mcp-abap-adt/header-validator` ^0.1.3 - Header validation
199
210
  - `axios` - HTTP client
200
211
  - `zod` - Schema validation
201
-
package/README.md CHANGED
@@ -157,8 +157,17 @@ The proxy is transparent - it only adds authentication headers and forwards requ
157
157
  export MCP_HTTP_PORT=3001
158
158
  export LOG_LEVEL=info
159
159
  export MCP_PROXY_UNSAFE=true # Enable file-based session storage (optional)
160
+ export AUTH_BROKER_PATH=~/.config/mcp-abap-adt # Optional base path for service-keys/sessions
160
161
  ```
161
162
 
163
+ `AUTH_BROKER_PATH` is treated as a base directory. The proxy resolves:
164
+ - `service-keys` from `<AUTH_BROKER_PATH>/service-keys`
165
+ - `sessions` from `<AUTH_BROKER_PATH>/sessions`
166
+
167
+ Defaults when `AUTH_BROKER_PATH` is not set:
168
+ - Unix/Linux/macOS: `~/.config/mcp-abap-adt/service-keys` and `~/.config/mcp-abap-adt/sessions`
169
+ - Windows: `%USERPROFILE%\\Documents\\mcp-abap-adt\\service-keys` and `%USERPROFILE%\\Documents\\mcp-abap-adt\\sessions`
170
+
162
171
  ### Configuration File
163
172
 
164
173
  Create `mcp-proxy-config.json`:
@@ -175,7 +184,7 @@ Create `mcp-proxy-config.json`:
175
184
 
176
185
  **Session Storage:**
177
186
  - `unsafe: false` (default) - Session data stored in-memory (secure, lost on restart)
178
- - `unsafe: true` - Session data persisted to disk (tokens saved to `.env` files)
187
+ - `unsafe: true` - Session data persisted to disk (tokens saved under the session store path)
179
188
 
180
189
  See [Configuration Guide](./docs/CONFIGURATION.md) for complete options.
181
190
 
@@ -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
@@ -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
 
@@ -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', {
@@ -27,6 +27,24 @@ export interface ProxyConfig {
27
27
  * - If --config is NOT provided, load ONLY from command line parameters and environment variables
28
28
  */
29
29
  export declare function loadConfig(configPath?: string): ProxyConfig;
30
+ /**
31
+ * Transport-related fields from config file (not part of ProxyConfig but needed by transportConfig)
32
+ */
33
+ export interface FileTransportFields {
34
+ transport?: string;
35
+ httpPort?: number;
36
+ httpHost?: string;
37
+ ssePort?: number;
38
+ sseHost?: string;
39
+ }
40
+ /**
41
+ * Extract transport-related fields from a raw config object
42
+ */
43
+ export declare function extractTransportFields(raw: Record<string, unknown>): FileTransportFields;
44
+ /**
45
+ * Load raw config from file for transport field extraction
46
+ */
47
+ export declare function loadRawConfigFile(filePath: string): Record<string, unknown> | null;
30
48
  /**
31
49
  * Get config file path from command line (--config or -c)
32
50
  */
@@ -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;IAGnB,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;AAuHD;;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;IAGnB,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;AA0GD;;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"}
@@ -37,6 +37,8 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.loadConfig = loadConfig;
40
+ exports.extractTransportFields = extractTransportFields;
41
+ exports.loadRawConfigFile = loadRawConfigFile;
40
42
  exports.getConfigPath = getConfigPath;
41
43
  exports.validateConfig = validateConfig;
42
44
  const fs = __importStar(require("node:fs"));
@@ -80,6 +82,18 @@ function loadConfig(configPath) {
80
82
  // No --config provided: load ONLY from command line parameters and environment variables
81
83
  return loadFromEnv();
82
84
  }
85
+ /**
86
+ * Extract transport-related fields from a raw config object
87
+ */
88
+ function extractTransportFields(raw) {
89
+ return {
90
+ transport: typeof raw.transport === 'string' ? raw.transport : undefined,
91
+ httpPort: typeof raw.httpPort === 'number' ? raw.httpPort : undefined,
92
+ httpHost: typeof raw.httpHost === 'string' ? raw.httpHost : undefined,
93
+ ssePort: typeof raw.ssePort === 'number' ? raw.ssePort : undefined,
94
+ sseHost: typeof raw.sseHost === 'string' ? raw.sseHost : undefined,
95
+ };
96
+ }
83
97
  /**
84
98
  * Load configuration from file (supports JSON and YAML)
85
99
  */
@@ -93,6 +107,24 @@ function loadConfigFile(filePath) {
93
107
  return JSON.parse(configContent);
94
108
  }
95
109
  }
110
+ /**
111
+ * Load raw config from file for transport field extraction
112
+ */
113
+ function loadRawConfigFile(filePath) {
114
+ try {
115
+ if (!fs.existsSync(filePath))
116
+ return null;
117
+ const content = fs.readFileSync(filePath, 'utf-8');
118
+ const ext = path.extname(filePath).toLowerCase();
119
+ if (ext === '.yaml' || ext === '.yml') {
120
+ return yaml.load(content) || {};
121
+ }
122
+ return JSON.parse(content);
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
96
128
  /**
97
129
  * Apply default values to partial config (used when loading from file)
98
130
  */
@@ -104,6 +136,7 @@ function applyDefaults(fileConfig) {
104
136
  sseHost: fileConfig.sseHost || '0.0.0.0',
105
137
  logLevel: fileConfig.logLevel || 'info',
106
138
  btpDestination: fileConfig.btpDestination,
139
+ targetUrl: fileConfig.targetUrl,
107
140
  unsafe: fileConfig.unsafe ?? false,
108
141
  maxRetries: fileConfig.maxRetries ?? 3,
109
142
  retryDelay: fileConfig.retryDelay ?? 1000,
@@ -4,6 +4,20 @@
4
4
  * depending on the unsafe configuration parameter
5
5
  */
6
6
  import type { IServiceKeyStore, ISessionStore } from '@mcp-abap-adt/auth-broker';
7
+ /**
8
+ * Get platform-specific default paths for service keys and sessions
9
+ *
10
+ * Priority (matching mcp-abap-adt logic):
11
+ * 1. AUTH_BROKER_PATH environment variable
12
+ * 2. Platform-specific standard paths (only if no env paths were found):
13
+ * - Unix: ~/.config/mcp-abap-adt/{subfolder}
14
+ * - Windows: %USERPROFILE%\Documents\mcp-abap-adt\{subfolder}
15
+ * 3. Current working directory (process.cwd())
16
+ *
17
+ * @param subfolder Subfolder name ('service-keys' or 'sessions')
18
+ * @returns Array of resolved absolute paths
19
+ */
20
+ export declare function getPlatformPaths(subfolder?: 'service-keys' | 'sessions'): string[];
7
21
  /**
8
22
  * Get platform-specific stores
9
23
  * Returns XSUAA stores for BTP authentication:
@@ -1 +1 @@
1
- {"version":3,"file":"stores.d.ts","sourceRoot":"","sources":["../../src/lib/stores.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACd,MAAM,2BAA2B,CAAC;AA4EnC;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC;IACxE,eAAe,EAAE,gBAAgB,CAAC;IAClC,YAAY,EAAE,aAAa,CAAC;CAC7B,CAAC,CAqBD"}
1
+ {"version":3,"file":"stores.d.ts","sourceRoot":"","sources":["../../src/lib/stores.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACd,MAAM,2BAA2B,CAAC;AAOnC;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,CAAC,EAAE,cAAc,GAAG,UAAU,GACtC,MAAM,EAAE,CAgEV;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC;IACxE,eAAe,EAAE,gBAAgB,CAAC;IAClC,YAAY,EAAE,aAAa,CAAC;CAC7B,CAAC,CAqBD"}