@mcp-abap-adt/proxy 0.1.4 → 0.1.8

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/README.md +73 -63
  3. package/bin/mcp-abap-adt-proxy.js +15 -42
  4. package/dist/index.d.ts +3 -12
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +440 -273
  7. package/dist/lib/config.d.ts +13 -4
  8. package/dist/lib/config.d.ts.map +1 -1
  9. package/dist/lib/config.js +110 -92
  10. package/dist/lib/errorHandler.d.ts +3 -3
  11. package/dist/lib/errorHandler.d.ts.map +1 -1
  12. package/dist/lib/errorHandler.js +31 -28
  13. package/dist/lib/logger.d.ts.map +1 -1
  14. package/dist/lib/logger.js +15 -1
  15. package/dist/lib/stores.d.ts +7 -6
  16. package/dist/lib/stores.d.ts.map +1 -1
  17. package/dist/lib/stores.js +121 -8
  18. package/dist/lib/transportConfig.d.ts +1 -1
  19. package/dist/lib/transportConfig.d.ts.map +1 -1
  20. package/dist/lib/transportConfig.js +58 -38
  21. package/dist/proxy/btpProxy.d.ts +102 -0
  22. package/dist/proxy/btpProxy.d.ts.map +1 -0
  23. package/dist/proxy/btpProxy.js +833 -0
  24. package/dist/proxy/cloudLlmHubProxy.d.ts +1 -60
  25. package/dist/proxy/cloudLlmHubProxy.d.ts.map +1 -1
  26. package/dist/proxy/cloudLlmHubProxy.js +1 -378
  27. package/dist/router/headerAnalyzer.d.ts +14 -18
  28. package/dist/router/headerAnalyzer.d.ts.map +1 -1
  29. package/dist/router/headerAnalyzer.js +40 -67
  30. package/dist/router/requestInterceptor.d.ts +5 -11
  31. package/dist/router/requestInterceptor.d.ts.map +1 -1
  32. package/dist/router/requestInterceptor.js +33 -30
  33. package/package.json +33 -14
  34. package/dist/router/directCloudRouter.d.ts +0 -32
  35. package/dist/router/directCloudRouter.d.ts.map +0 -1
  36. package/dist/router/directCloudRouter.js +0 -179
  37. package/dist/router/localBasicRouter.d.ts +0 -23
  38. package/dist/router/localBasicRouter.d.ts.map +0 -1
  39. package/dist/router/localBasicRouter.js +0 -140
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  "use strict";
3
2
  /**
4
3
  * MCP ABAP ADT Proxy Server
@@ -6,24 +5,31 @@
6
5
  * Proxies local MCP requests to MCP servers with optional JWT authentication.
7
6
  * Routes requests based on authentication headers:
8
7
  * - x-btp-destination (or --btp): BTP Cloud authorization with JWT token
9
- * - x-mcp-destination (or --mcp): SAP ABAP connection configuration
10
8
  *
11
- * Supports two modes:
12
- * 1. BTP authentication mode: Requires x-btp-destination or --btp parameter
13
- * 2. Local testing mode: Works with only x-mcp-destination or --mcp parameter (no BTP authentication)
9
+ * Supports BTP authentication mode: Requires x-btp-destination or --btp parameter
14
10
  */
15
11
  Object.defineProperty(exports, "__esModule", { value: true });
16
12
  exports.McpAbapAdtProxyServer = void 0;
13
+ const node_http_1 = require("node:http");
14
+ const interfaces_1 = require("@mcp-abap-adt/interfaces");
17
15
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
18
- const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
19
16
  const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
20
- const http_1 = require("http");
21
- const transportConfig_js_1 = require("./lib/transportConfig.js");
17
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
22
18
  const config_js_1 = require("./lib/config.js");
23
19
  const logger_js_1 = require("./lib/logger.js");
24
- const requestInterceptor_js_1 = require("./router/requestInterceptor.js");
20
+ const transportConfig_js_1 = require("./lib/transportConfig.js");
21
+ const btpProxy_js_1 = require("./proxy/btpProxy.js");
25
22
  const headerAnalyzer_js_1 = require("./router/headerAnalyzer.js");
26
- const cloudLlmHubProxy_js_1 = require("./proxy/cloudLlmHubProxy.js");
23
+ const requestInterceptor_js_1 = require("./router/requestInterceptor.js");
24
+ /**
25
+ * Safely extract id from unknown body object
26
+ */
27
+ function getBodyId(body) {
28
+ if (body && typeof body === 'object' && body !== null && 'id' in body) {
29
+ return body.id;
30
+ }
31
+ return null;
32
+ }
27
33
  /**
28
34
  * MCP ABAP ADT Proxy Server
29
35
  */
@@ -32,30 +38,31 @@ class McpAbapAdtProxyServer {
32
38
  transportConfig;
33
39
  config;
34
40
  httpServer;
35
- cloudLlmHubProxy;
41
+ btpProxy;
36
42
  constructor(transportConfig, configPath) {
37
43
  this.transportConfig = transportConfig || (0, transportConfig_js_1.parseTransportConfig)();
38
44
  this.config = (0, config_js_1.loadConfig)(configPath);
39
45
  // Validate configuration
40
- if (this.transportConfig.type === "streamable-http" || this.transportConfig.type === "sse") {
46
+ if (this.transportConfig.type === 'streamable-http' ||
47
+ this.transportConfig.type === 'sse') {
41
48
  const validation = (0, config_js_1.validateConfig)(this.config);
42
49
  if (!validation.valid) {
43
- logger_js_1.logger.error("Configuration validation failed", {
44
- type: "CONFIG_VALIDATION_ERROR",
50
+ logger_js_1.logger?.error('Configuration validation failed', {
51
+ type: 'CONFIG_VALIDATION_ERROR',
45
52
  errors: validation.errors,
46
53
  });
47
- throw new Error(`Configuration validation failed: ${validation.errors.join(", ")}`);
54
+ throw new Error(`Configuration validation failed: ${validation.errors.join(', ')}`);
48
55
  }
49
56
  if (validation.warnings.length > 0) {
50
- logger_js_1.logger.warn("Configuration validation warnings", {
51
- type: "CONFIG_VALIDATION_WARNINGS",
57
+ logger_js_1.logger?.warn('Configuration validation warnings', {
58
+ type: 'CONFIG_VALIDATION_WARNINGS',
52
59
  warnings: validation.warnings,
53
60
  });
54
61
  }
55
62
  }
56
63
  this.server = new mcp_js_1.McpServer({
57
- name: "@mcp-abap-adt/proxy",
58
- version: "0.1.0",
64
+ name: '@mcp-abap-adt/proxy',
65
+ version: '0.1.0',
59
66
  }, {
60
67
  capabilities: {
61
68
  tools: {},
@@ -71,92 +78,21 @@ class McpAbapAdtProxyServer {
71
78
  // The server automatically handles initialize and tools/list requests
72
79
  // For stdio, we'll intercept requests through server.server message handler
73
80
  }
74
- /**
75
- * Handle stdio request by proxying to MCP server
76
- * Supports both BTP authentication mode (--btp) and local testing mode (--mcp without --btp)
77
- */
78
- async handleStdioRequest(method, params, id) {
79
- // Create fake headers with config overrides (--btp, --mcp, and/or --mcp-url)
80
- const fakeHeaders = {};
81
- if (this.config.btpDestination) {
82
- fakeHeaders["x-btp-destination"] = this.config.btpDestination;
83
- }
84
- if (this.config.mcpDestination) {
85
- fakeHeaders["x-mcp-destination"] = this.config.mcpDestination;
86
- }
87
- if (this.config.mcpUrl) {
88
- fakeHeaders["x-mcp-url"] = this.config.mcpUrl;
89
- }
90
- // Create routing decision using config overrides
91
- const configOverrides = {
92
- btpDestination: this.config.btpDestination,
93
- mcpDestination: this.config.mcpDestination,
94
- mcpUrl: this.config.mcpUrl,
95
- };
96
- const { analyzeHeaders } = await import("./router/headerAnalyzer.js");
97
- const routingDecision = analyzeHeaders(fakeHeaders, configOverrides);
98
- // Check if routing is valid
99
- if (routingDecision.strategy !== headerAnalyzer_js_1.RoutingStrategy.PROXY) {
100
- logger_js_1.logger.error("Routing decision failed for stdio request", {
101
- type: "STDIO_ROUTING_DECISION_FAILED",
102
- reason: routingDecision.reason,
103
- });
104
- return {
105
- jsonrpc: "2.0",
106
- id: id || null,
107
- error: {
108
- code: -32602,
109
- message: routingDecision.reason || "Routing decision failed",
110
- },
111
- };
112
- }
113
- // Ensure proxy is initialized
114
- if (!this.cloudLlmHubProxy) {
115
- const baseUrl = this.config.cloudLlmHubUrl || "https://default.example.com";
116
- this.cloudLlmHubProxy = await (0, cloudLlmHubProxy_js_1.createCloudLlmHubProxy)(baseUrl, this.config);
117
- }
118
- // Build MCP request
119
- const mcpRequest = {
120
- method: method || "",
121
- params: params || {},
122
- id: id || null,
123
- jsonrpc: "2.0",
124
- };
125
- // Proxy request to cloud-llm-hub
126
- try {
127
- const proxyResponse = await this.cloudLlmHubProxy.proxyRequest(mcpRequest, routingDecision, fakeHeaders);
128
- return proxyResponse;
129
- }
130
- catch (error) {
131
- logger_js_1.logger.error("Failed to proxy stdio request", {
132
- type: "STDIO_PROXY_ERROR",
133
- error: error instanceof Error ? error.message : String(error),
134
- });
135
- return {
136
- jsonrpc: "2.0",
137
- id: id || null,
138
- error: {
139
- code: -32000,
140
- message: error instanceof Error ? error.message : "Unknown error",
141
- },
142
- };
143
- }
144
- }
145
81
  /**
146
82
  * Run the proxy server
147
83
  */
148
84
  async run() {
149
- if (this.transportConfig.type === "stdio") {
150
- // Check if either --btp, --mcp, or --mcp-url parameter is provided (required for stdio)
151
- if (!this.config.btpDestination && !this.config.mcpDestination && !this.config.mcpUrl) {
152
- logger_js_1.logger.error("Either --btp, --mcp, or --mcp-url parameter is required for stdio transport", {
153
- type: "STDIO_DESTINATION_REQUIRED",
85
+ if (this.transportConfig.type === 'stdio') {
86
+ // Check if --btp parameter is provided (required for stdio)
87
+ if (!this.config.btpDestination) {
88
+ logger_js_1.logger?.error('--btp parameter is required for stdio transport', {
89
+ type: 'STDIO_DESTINATION_REQUIRED',
154
90
  });
155
- throw new Error("Either --btp, --mcp, or --mcp-url parameter is required for stdio transport. Use --btp=<destination> for BTP destination, --mcp=<destination> for MCP destination, or --mcp-url=<url> for direct MCP server URL (local testing).");
91
+ throw new Error('--btp parameter is required for stdio transport. Use --btp=<destination> for BTP destination.');
156
92
  }
157
93
  // For stdio, we need to register a proxy tool that forwards all requests
158
94
  // Since we can't intercept all requests directly, we'll register a tool
159
- // that the client can call, which will proxy to cloud-llm-hub
95
+ // that the client can call, which will proxy to BtpProxy
160
96
  // Note: This requires the client to know about the proxy tool
161
97
  // Alternatively, we could use a custom transport wrapper
162
98
  // Simple stdio setup - MCP SDK will handle JSON-RPC protocol
@@ -166,23 +102,38 @@ class McpAbapAdtProxyServer {
166
102
  // For now, we'll just connect and let the SDK handle it
167
103
  // The actual proxying will need to be done through registered tools
168
104
  const transport = new stdio_js_1.StdioServerTransport();
169
- await this.server.server.connect(transport);
170
- logger_js_1.logger.info("MCP Proxy Server started (stdio transport)", {
171
- type: "SERVER_STARTED",
172
- transport: "stdio",
105
+ const msg = `MCP Proxy Server started (Transport: stdio)
106
+ Authorization source: BTP destination "${this.config.btpDestination}"`;
107
+ console.error(msg);
108
+ logger_js_1.logger?.info('MCP Proxy Server started (stdio transport)', {
109
+ type: 'SERVER_STARTED',
110
+ transport: 'stdio',
173
111
  btpDestination: this.config.btpDestination,
174
- mcpDestination: this.config.mcpDestination,
175
- mcpUrl: this.config.mcpUrl,
176
- note: "For stdio transport, --btp, --mcp, and/or --mcp-url parameters are used as default destinations",
177
- mode: this.config.btpDestination ? "BTP authentication" : "Local testing (no BTP authentication)",
112
+ note: 'For stdio transport, --btp parameter is used as default destination',
113
+ mode: 'BTP authentication',
178
114
  });
179
115
  return;
180
116
  }
181
- if (this.transportConfig.type === "streamable-http") {
117
+ // Eager initialization of BTP authentication
118
+ if (this.config.btpDestination) {
119
+ try {
120
+ if (!this.btpProxy) {
121
+ this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
122
+ }
123
+ // Trigger initialization (background) - this will open the browser
124
+ this.btpProxy.initialize(this.config.btpDestination);
125
+ }
126
+ catch (error) {
127
+ logger_js_1.logger?.error('Failed to initialize BTP proxy for eager auth', {
128
+ error: error instanceof Error ? error.message : String(error),
129
+ });
130
+ }
131
+ }
132
+ if (this.transportConfig.type === 'streamable-http') {
182
133
  await this.startHttpServer();
183
134
  return;
184
135
  }
185
- if (this.transportConfig.type === "sse") {
136
+ if (this.transportConfig.type === 'sse') {
186
137
  await this.startSseServer();
187
138
  return;
188
139
  }
@@ -193,25 +144,23 @@ class McpAbapAdtProxyServer {
193
144
  */
194
145
  async startHttpServer() {
195
146
  const httpConfig = this.transportConfig;
196
- this.httpServer = (0, http_1.createServer)(async (req, res) => {
197
- // Log incoming HTTP request (if debug enabled)
198
- const debugHttpEnabled = process.env.DEBUG_HTTP_REQUESTS === "true";
199
- if (debugHttpEnabled) {
200
- const sanitizedHeaders = (0, requestInterceptor_js_1.sanitizeHeadersForLogging)(req.headers);
201
- logger_js_1.logger.info("HTTP Request received", {
202
- type: "HTTP_REQUEST",
203
- method: req.method,
204
- url: req.url,
205
- headers: sanitizedHeaders,
206
- remoteAddress: req.socket.remoteAddress,
207
- });
208
- }
209
- // Only handle POST requests
210
- if (req.method !== "POST") {
211
- res.writeHead(405, { "Content-Type": "text/plain" });
212
- res.end("Method not allowed");
213
- return;
214
- }
147
+ 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';
151
+ const sanitizedHeaders = (0, requestInterceptor_js_1.sanitizeHeadersForLogging)(req.headers);
152
+ logger_js_1.logger?.info('=== HTTP REQUEST RECEIVED ===', {
153
+ type: 'HTTP_REQUEST_RECEIVED',
154
+ method: req.method,
155
+ url: req.url,
156
+ headers: sanitizedHeaders,
157
+ remoteAddress: req.socket.remoteAddress,
158
+ });
159
+ // Allow all HTTP methods (GET, POST, PUT, DELETE, etc.)
160
+ // if (req.method !== 'POST') {
161
+ // logger?.warn('Non-POST request rejected', { ... });
162
+ // ...
163
+ // }
215
164
  // Read request body
216
165
  let body;
217
166
  try {
@@ -219,36 +168,93 @@ class McpAbapAdtProxyServer {
219
168
  for await (const chunk of req) {
220
169
  chunks.push(chunk);
221
170
  }
222
- const bodyString = Buffer.concat(chunks).toString("utf-8");
223
- body = JSON.parse(bodyString);
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
224
  }
225
225
  catch (error) {
226
- logger_js_1.logger.error("Failed to parse request body", {
227
- type: "REQUEST_PARSE_ERROR",
226
+ logger_js_1.logger?.error('=== FAILED TO PARSE REQUEST BODY ===', {
227
+ type: 'REQUEST_PARSE_ERROR',
228
228
  error: error instanceof Error ? error.message : String(error),
229
+ errorStack: error instanceof Error ? error.stack : undefined,
229
230
  });
230
- res.writeHead(400, { "Content-Type": "text/plain" });
231
- res.end("Invalid request body");
231
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
232
+ res.end('Invalid request body');
232
233
  return;
233
234
  }
234
235
  // Intercept and analyze request
235
- // Pass config overrides (--btp, --mcp, and --mcp-url) to header analyzer
236
236
  const configOverrides = {
237
237
  btpDestination: this.config.btpDestination,
238
- mcpDestination: this.config.mcpDestination,
239
- mcpUrl: this.config.mcpUrl,
238
+ targetUrl: this.config.targetUrl,
240
239
  };
240
+ logger_js_1.logger?.info('=== INTERCEPTING REQUEST ===', {
241
+ type: 'REQUEST_INTERCEPTING',
242
+ configOverrides,
243
+ });
241
244
  const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
242
245
  // Check routing decision
243
246
  if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
244
- logger_js_1.logger.error("Routing decision failed", {
245
- type: "ROUTING_DECISION_FAILED",
247
+ logger_js_1.logger?.error('Routing decision failed', {
248
+ type: 'ROUTING_DECISION_FAILED',
246
249
  reason: intercepted.routingDecision.reason,
247
250
  });
248
- res.writeHead(400, { "Content-Type": "application/json" });
251
+ res.writeHead(400, { 'Content-Type': 'application/json' });
252
+ const bodyId = body && typeof body === 'object' && body !== null && 'id' in body
253
+ ? body.id
254
+ : null;
249
255
  res.end(JSON.stringify({
250
- jsonrpc: "2.0",
251
- id: body?.id || null,
256
+ jsonrpc: '2.0',
257
+ id: bodyId || null,
252
258
  error: {
253
259
  code: -32602,
254
260
  message: intercepted.routingDecision.reason,
@@ -257,43 +263,47 @@ class McpAbapAdtProxyServer {
257
263
  return;
258
264
  }
259
265
  // Log proxy request
260
- logger_js_1.logger.debug("Proxying request", {
261
- type: "PROXY_REQUEST",
266
+ logger_js_1.logger?.info('=== PROXYING REQUEST ===', {
267
+ type: 'PROXY_REQUEST_START',
262
268
  btpDestination: intercepted.routingDecision.btpDestination,
263
- mcpDestination: intercepted.routingDecision.mcpDestination,
264
- requiresSapConfig: (0, requestInterceptor_js_1.requiresSapConfig)(body),
265
269
  });
266
270
  // Handle proxy request - add JWT and forward to x-mcp-url
267
271
  try {
268
272
  await this.handleProxyRequest(intercepted, req, res);
269
273
  }
270
274
  catch (error) {
271
- logger_js_1.logger.error("Failed to process request", {
272
- type: "REQUEST_PROCESS_ERROR",
275
+ logger_js_1.logger?.error('Failed to process request', {
276
+ type: 'REQUEST_PROCESS_ERROR',
273
277
  error: error instanceof Error ? error.message : String(error),
274
278
  strategy: intercepted.routingDecision.strategy,
275
279
  });
276
280
  if (!res.headersSent) {
277
- res.writeHead(500, { "Content-Type": "text/plain" });
278
- res.end("Internal server error");
281
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
282
+ res.end('Internal server error');
279
283
  }
280
284
  }
281
285
  });
282
286
  const port = httpConfig.port || 3001;
283
- const host = httpConfig.host || "0.0.0.0";
287
+ const host = httpConfig.host || '0.0.0.0';
284
288
  return new Promise((resolve, reject) => {
285
- this.httpServer.listen(port, host, () => {
286
- logger_js_1.logger.info("MCP Proxy Server started (HTTP transport)", {
287
- type: "SERVER_STARTED",
288
- transport: "streamable-http",
289
+ this.httpServer?.listen(port, host, () => {
290
+ const authSourceObj = this.config.btpDestination
291
+ ? `BTP destination "${this.config.btpDestination}"`
292
+ : 'Waiting for "x-btp-destination" header in requests';
293
+ const msg = `MCP Proxy Server started on port ${port} (Transport: http)
294
+ Authorization source: ${authSourceObj}`;
295
+ console.log(msg);
296
+ logger_js_1.logger?.info('MCP Proxy Server started (HTTP transport)', {
297
+ type: 'SERVER_STARTED',
298
+ transport: 'streamable-http',
289
299
  host,
290
300
  port,
291
301
  });
292
302
  resolve();
293
303
  });
294
- this.httpServer.on("error", (error) => {
295
- logger_js_1.logger.error("Failed to start HTTP server", {
296
- type: "SERVER_START_ERROR",
304
+ this.httpServer?.on('error', (error) => {
305
+ logger_js_1.logger?.error('Failed to start HTTP server', {
306
+ type: 'SERVER_START_ERROR',
297
307
  error: error.message,
298
308
  });
299
309
  reject(error);
@@ -305,55 +315,187 @@ class McpAbapAdtProxyServer {
305
315
  */
306
316
  async handleProxyRequest(intercepted, req, res) {
307
317
  const btpDestination = intercepted.routingDecision.btpDestination;
308
- const mcpDestination = intercepted.routingDecision.mcpDestination;
309
- const mcpUrl = intercepted.routingDecision.mcpUrl;
310
- logger_js_1.logger.info("Handling proxy request", {
311
- type: "PROXY_REQUEST",
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
+ }
333
+ }
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;
373
+ }
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,
312
381
  btpDestination,
313
- mcpDestination,
314
- mcpUrl,
315
382
  sessionId: intercepted.sessionId,
316
- mode: btpDestination ? "BTP authentication" : "Local testing (no BTP authentication)",
383
+ headers: incomingHeaders,
384
+ body: sanitizedBody,
385
+ mode: 'BTP authentication',
317
386
  });
318
387
  try {
319
388
  // Ensure proxy is initialized (URL will be obtained from service key for btpDestination or mcpDestination)
320
- if (!this.cloudLlmHubProxy) {
321
- // Use default base URL from config as fallback (actual URL comes from service key)
322
- const baseUrl = this.config.cloudLlmHubUrl || "https://default.example.com";
323
- this.cloudLlmHubProxy = await (0, cloudLlmHubProxy_js_1.createCloudLlmHubProxy)(baseUrl, this.config);
389
+ if (!this.btpProxy) {
390
+ this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
324
391
  }
325
392
  // Build MCP request from intercepted request
326
- const mcpRequest = {
327
- method: intercepted.body?.method || "",
328
- params: intercepted.body?.params || {},
329
- id: intercepted.body?.id || null,
330
- jsonrpc: intercepted.body?.jsonrpc || "2.0",
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
331
426
  };
332
- // Proxy request to cloud-llm-hub
333
- const proxyResponse = await this.cloudLlmHubProxy.proxyRequest(mcpRequest, intercepted.routingDecision, intercepted.headers);
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
+ });
334
467
  // Send response back to client
335
- res.writeHead(200, { "Content-Type": "application/json" });
468
+ res.writeHead(200, { 'Content-Type': 'application/json' });
336
469
  res.end(JSON.stringify(proxyResponse));
337
- logger_js_1.logger.debug("Cloud-llm-hub proxy request completed", {
338
- type: "CLOUD_LLM_HUB_PROXY_COMPLETED",
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',
339
476
  hasResult: !!proxyResponse.result,
340
477
  hasError: !!proxyResponse.error,
341
478
  });
342
479
  }
343
480
  catch (error) {
344
- logger_js_1.logger.error("Failed to handle cloud-llm-hub proxy request", {
345
- type: "CLOUD_LLM_HUB_PROXY_ERROR",
346
- error: error instanceof Error ? error.message : String(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,
347
485
  stack: error instanceof Error ? error.stack : undefined,
348
486
  });
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
+ }
349
491
  if (!res.headersSent) {
350
- res.writeHead(500, { "Content-Type": "application/json" });
492
+ res.writeHead(500, { 'Content-Type': 'application/json' });
351
493
  res.end(JSON.stringify({
352
- jsonrpc: "2.0",
353
- id: intercepted.body?.id || null,
494
+ jsonrpc: '2.0',
495
+ id: getBodyId(intercepted.body) || null,
354
496
  error: {
355
497
  code: -32000,
356
- message: error instanceof Error ? error.message : "Unknown error",
498
+ message: errorMessage,
357
499
  },
358
500
  }));
359
501
  }
@@ -365,36 +507,38 @@ class McpAbapAdtProxyServer {
365
507
  async startSseServer() {
366
508
  const sseConfig = this.transportConfig;
367
509
  const streamPathMap = new Map([
368
- ["/", "/messages"],
369
- ["/mcp/events", "/mcp/messages"],
370
- ["/sse", "/messages"],
510
+ ['/', '/messages'],
511
+ ['/mcp/events', '/mcp/messages'],
512
+ ['/sse', '/messages'],
371
513
  ]);
372
- const streamPaths = Array.from(streamPathMap.keys());
514
+ const _streamPaths = Array.from(streamPathMap.keys());
373
515
  const postPathSet = new Set(streamPathMap.values());
374
- postPathSet.add("/messages");
375
- postPathSet.add("/mcp/messages");
376
- const httpServer = (0, http_1.createServer)(async (req, res) => {
516
+ postPathSet.add('/messages');
517
+ postPathSet.add('/mcp/messages');
518
+ const httpServer = (0, node_http_1.createServer)(async (req, res) => {
377
519
  // SSE: Always restrict to local connections only
378
520
  const remoteAddress = req.socket.remoteAddress;
379
- const isLocal = remoteAddress === "127.0.0.1" ||
380
- remoteAddress === "::1" ||
381
- remoteAddress === "localhost" ||
382
- (remoteAddress && remoteAddress.startsWith("127."));
521
+ const isLocal = remoteAddress === '127.0.0.1' ||
522
+ remoteAddress === '::1' ||
523
+ remoteAddress === 'localhost' ||
524
+ remoteAddress?.startsWith('127.');
383
525
  if (!isLocal) {
384
- logger_js_1.logger.warn("SSE: Non-local connection rejected", {
385
- type: "SSE_NON_LOCAL_REJECTED",
526
+ logger_js_1.logger?.warn('SSE: Non-local connection rejected', {
527
+ type: 'SSE_NON_LOCAL_REJECTED',
386
528
  remoteAddress,
387
529
  });
388
- res.writeHead(403, { "Content-Type": "text/plain" });
389
- res.end("Forbidden: SSE transport only accepts local connections");
530
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
531
+ res.end('Forbidden: SSE transport only accepts local connections');
390
532
  return;
391
533
  }
392
- const requestUrl = req.url ? new URL(req.url, `http://${req.headers.host ?? `${sseConfig.host}:${sseConfig.port}`}`) : undefined;
393
- let pathname = requestUrl?.pathname ?? "/";
394
- if (pathname.length > 1 && pathname.endsWith("/")) {
534
+ const requestUrl = req.url
535
+ ? new URL(req.url, `http://${req.headers.host ?? `${sseConfig.host}:${sseConfig.port}`}`)
536
+ : undefined;
537
+ let pathname = requestUrl?.pathname ?? '/';
538
+ if (pathname.length > 1 && pathname.endsWith('/')) {
395
539
  pathname = pathname.slice(0, -1);
396
540
  }
397
- // Apply config overrides (--btp, --mcp, and --mcp-url) to headers if not present
541
+ // Apply config overrides (--btp) to headers if not present
398
542
  const headers = {};
399
543
  for (const [key, value] of Object.entries(req.headers)) {
400
544
  if (value) {
@@ -402,33 +546,27 @@ class McpAbapAdtProxyServer {
402
546
  }
403
547
  }
404
548
  // Add config overrides if not present in headers
405
- if (this.config.btpDestination && !headers["x-btp-destination"]) {
406
- headers["x-btp-destination"] = this.config.btpDestination;
407
- }
408
- if (this.config.mcpDestination && !headers["x-mcp-destination"]) {
409
- headers["x-mcp-destination"] = this.config.mcpDestination;
410
- }
411
- if (this.config.mcpUrl && !headers["x-mcp-url"]) {
412
- headers["x-mcp-url"] = this.config.mcpUrl;
549
+ if (this.config.btpDestination && !headers[interfaces_1.HEADER_BTP_DESTINATION]) {
550
+ headers[interfaces_1.HEADER_BTP_DESTINATION] = this.config.btpDestination;
413
551
  }
414
- logger_js_1.logger.debug("SSE request received", {
415
- type: "SSE_HTTP_REQUEST",
552
+ logger_js_1.logger?.debug('SSE request received', {
553
+ type: 'SSE_HTTP_REQUEST',
416
554
  method: req.method,
417
555
  pathname,
418
556
  originalUrl: req.url,
419
557
  });
420
558
  // GET /sse, /mcp/events, or / - establish SSE connection
421
- if (req.method === "GET" && streamPathMap.has(pathname)) {
422
- const postEndpoint = streamPathMap.get(pathname) ?? "/messages";
423
- logger_js_1.logger.debug("SSE client connecting", {
424
- type: "SSE_CLIENT_CONNECTING",
559
+ if (req.method === 'GET' && streamPathMap.has(pathname)) {
560
+ const postEndpoint = streamPathMap.get(pathname) ?? '/messages';
561
+ logger_js_1.logger?.debug('SSE client connecting', {
562
+ type: 'SSE_CLIENT_CONNECTING',
425
563
  pathname,
426
564
  postEndpoint,
427
565
  });
428
566
  // Create new McpServer instance for this session
429
567
  const sessionServer = new mcp_js_1.McpServer({
430
- name: "@mcp-abap-adt/proxy",
431
- version: "0.1.0",
568
+ name: '@mcp-abap-adt/proxy',
569
+ version: '0.1.0',
432
570
  }, {
433
571
  capabilities: {
434
572
  tools: {},
@@ -441,29 +579,29 @@ class McpAbapAdtProxyServer {
441
579
  enableDnsRebindingProtection: sseConfig.enableDnsRebindingProtection,
442
580
  });
443
581
  const sessionId = transport.sessionId;
444
- logger_js_1.logger.info("New SSE session created", {
445
- type: "SSE_SESSION_CREATED",
582
+ logger_js_1.logger?.info('New SSE session created', {
583
+ type: 'SSE_SESSION_CREATED',
446
584
  sessionId,
447
585
  pathname,
448
586
  });
449
587
  // Connect transport to server
450
588
  try {
451
589
  await sessionServer.server.connect(transport);
452
- logger_js_1.logger.info("SSE transport connected", {
453
- type: "SSE_CONNECTION_READY",
590
+ logger_js_1.logger?.info('SSE transport connected', {
591
+ type: 'SSE_CONNECTION_READY',
454
592
  sessionId,
455
593
  pathname,
456
594
  postEndpoint,
457
595
  });
458
596
  }
459
597
  catch (error) {
460
- logger_js_1.logger.error("Failed to connect SSE transport", {
461
- type: "SSE_CONNECT_ERROR",
598
+ logger_js_1.logger?.error('Failed to connect SSE transport', {
599
+ type: 'SSE_CONNECT_ERROR',
462
600
  error: error instanceof Error ? error.message : String(error),
463
601
  sessionId,
464
602
  });
465
603
  if (!res.headersSent) {
466
- res.writeHead(500).end("Internal Server Error");
604
+ res.writeHead(500).end('Internal Server Error');
467
605
  }
468
606
  else {
469
607
  res.end();
@@ -471,17 +609,17 @@ class McpAbapAdtProxyServer {
471
609
  return;
472
610
  }
473
611
  // Cleanup on connection close
474
- res.on("close", () => {
475
- logger_js_1.logger.info("SSE connection closed", {
476
- type: "SSE_CONNECTION_CLOSED",
612
+ res.on('close', () => {
613
+ logger_js_1.logger?.info('SSE connection closed', {
614
+ type: 'SSE_CONNECTION_CLOSED',
477
615
  sessionId,
478
616
  pathname,
479
617
  });
480
618
  sessionServer.server.close();
481
619
  });
482
620
  transport.onerror = (error) => {
483
- logger_js_1.logger.error("SSE transport error", {
484
- type: "SSE_TRANSPORT_ERROR",
621
+ logger_js_1.logger?.error('SSE transport error', {
622
+ type: 'SSE_TRANSPORT_ERROR',
485
623
  error: error instanceof Error ? error.message : String(error),
486
624
  sessionId,
487
625
  });
@@ -489,30 +627,30 @@ class McpAbapAdtProxyServer {
489
627
  return;
490
628
  }
491
629
  // POST /messages or /mcp/messages - handle client messages
492
- if (req.method === "POST" && postPathSet.has(pathname)) {
630
+ if (req.method === 'POST' && postPathSet.has(pathname)) {
493
631
  // Extract sessionId from query string or header
494
632
  let sessionId;
495
633
  if (requestUrl) {
496
- sessionId = requestUrl.searchParams.get("sessionId") || undefined;
634
+ sessionId = requestUrl.searchParams.get('sessionId') || undefined;
497
635
  }
498
636
  if (!sessionId) {
499
- sessionId = req.headers["x-session-id"];
637
+ sessionId = req.headers['x-session-id'];
500
638
  }
501
- logger_js_1.logger.debug("SSE POST request received", {
502
- type: "SSE_POST_REQUEST",
639
+ logger_js_1.logger?.debug('SSE POST request received', {
640
+ type: 'SSE_POST_REQUEST',
503
641
  sessionId,
504
642
  pathname,
505
643
  });
506
644
  if (!sessionId) {
507
- logger_js_1.logger.error("Missing sessionId in SSE POST request", {
508
- type: "SSE_MISSING_SESSION_ID",
645
+ logger_js_1.logger?.error('Missing sessionId in SSE POST request', {
646
+ type: 'SSE_MISSING_SESSION_ID',
509
647
  pathname,
510
648
  });
511
- res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({
512
- jsonrpc: "2.0",
649
+ res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({
650
+ jsonrpc: '2.0',
513
651
  error: {
514
652
  code: -32000,
515
- message: "Missing sessionId",
653
+ message: 'Missing sessionId',
516
654
  },
517
655
  id: null,
518
656
  }));
@@ -526,20 +664,20 @@ class McpAbapAdtProxyServer {
526
664
  chunks.push(chunk);
527
665
  }
528
666
  if (chunks.length > 0) {
529
- const bodyString = Buffer.concat(chunks).toString("utf-8");
667
+ const bodyString = Buffer.concat(chunks).toString('utf-8');
530
668
  body = JSON.parse(bodyString);
531
669
  }
532
670
  }
533
671
  catch (error) {
534
- logger_js_1.logger.error("Failed to parse SSE POST request body", {
535
- type: "SSE_POST_PARSE_ERROR",
672
+ logger_js_1.logger?.error('Failed to parse SSE POST request body', {
673
+ type: 'SSE_POST_PARSE_ERROR',
536
674
  error: error instanceof Error ? error.message : String(error),
537
675
  });
538
- res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({
539
- jsonrpc: "2.0",
676
+ res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({
677
+ jsonrpc: '2.0',
540
678
  error: {
541
679
  code: -32700,
542
- message: "Parse error",
680
+ message: 'Parse error',
543
681
  },
544
682
  id: null,
545
683
  }));
@@ -548,19 +686,17 @@ class McpAbapAdtProxyServer {
548
686
  // Intercept and analyze request with config overrides
549
687
  const configOverrides = {
550
688
  btpDestination: this.config.btpDestination,
551
- mcpDestination: this.config.mcpDestination,
552
- mcpUrl: this.config.mcpUrl,
553
689
  };
554
690
  const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
555
691
  // Check routing decision
556
692
  if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
557
- logger_js_1.logger.error("Routing decision failed for SSE POST", {
558
- type: "SSE_POST_ROUTING_FAILED",
693
+ logger_js_1.logger?.error('Routing decision failed for SSE POST', {
694
+ type: 'SSE_POST_ROUTING_FAILED',
559
695
  reason: intercepted.routingDecision.reason,
560
696
  });
561
- res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({
562
- jsonrpc: "2.0",
563
- id: body?.id || null,
697
+ res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({
698
+ jsonrpc: '2.0',
699
+ id: getBodyId(body) || null,
564
700
  error: {
565
701
  code: -32602,
566
702
  message: intercepted.routingDecision.reason,
@@ -573,18 +709,18 @@ class McpAbapAdtProxyServer {
573
709
  await this.handleProxyRequest(intercepted, req, res);
574
710
  }
575
711
  catch (error) {
576
- logger_js_1.logger.error("Failed to process SSE POST request", {
577
- type: "SSE_POST_PROCESS_ERROR",
712
+ logger_js_1.logger?.error('Failed to process SSE POST request', {
713
+ type: 'SSE_POST_PROCESS_ERROR',
578
714
  error: error instanceof Error ? error.message : String(error),
579
715
  });
580
716
  if (!res.headersSent) {
581
- res.writeHead(500, { "Content-Type": "application/json" });
717
+ res.writeHead(500, { 'Content-Type': 'application/json' });
582
718
  res.end(JSON.stringify({
583
- jsonrpc: "2.0",
584
- id: body?.id || null,
719
+ jsonrpc: '2.0',
720
+ id: getBodyId(body) || null,
585
721
  error: {
586
722
  code: -32000,
587
- message: error instanceof Error ? error.message : "Unknown error",
723
+ message: error instanceof Error ? error.message : 'Unknown error',
588
724
  },
589
725
  }));
590
726
  }
@@ -592,25 +728,31 @@ class McpAbapAdtProxyServer {
592
728
  return;
593
729
  }
594
730
  // Unknown path or method
595
- res.writeHead(404, { "Content-Type": "text/plain" });
596
- res.end("Not found");
731
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
732
+ res.end('Not found');
597
733
  });
598
734
  const port = sseConfig.port || 3002;
599
- const host = sseConfig.host || "0.0.0.0";
735
+ const host = sseConfig.host || '0.0.0.0';
600
736
  return new Promise((resolve, reject) => {
601
737
  httpServer.listen(port, host, () => {
602
- logger_js_1.logger.info("MCP Proxy Server started (SSE transport)", {
603
- type: "SERVER_STARTED",
604
- transport: "sse",
738
+ const authSourceObj = this.config.btpDestination
739
+ ? `BTP destination "${this.config.btpDestination}"`
740
+ : 'Waiting for "x-btp-destination" header in requests';
741
+ const msg = `MCP Proxy Server started on port ${port} (Transport: sse)
742
+ Authorization source: ${authSourceObj}`;
743
+ console.log(msg);
744
+ logger_js_1.logger?.info('MCP Proxy Server started (SSE transport)', {
745
+ type: 'SERVER_STARTED',
746
+ transport: 'sse',
605
747
  host,
606
748
  port,
607
749
  });
608
750
  this.httpServer = httpServer;
609
751
  resolve();
610
752
  });
611
- httpServer.on("error", (error) => {
612
- logger_js_1.logger.error("Failed to start SSE server", {
613
- type: "SERVER_START_ERROR",
753
+ httpServer.on('error', (error) => {
754
+ logger_js_1.logger?.error('Failed to start SSE server', {
755
+ type: 'SERVER_START_ERROR',
614
756
  error: error.message,
615
757
  });
616
758
  reject(error);
@@ -625,15 +767,15 @@ class McpAbapAdtProxyServer {
625
767
  await this.server.close();
626
768
  }
627
769
  catch (error) {
628
- logger_js_1.logger.error("Failed to close MCP server", {
629
- type: "SERVER_SHUTDOWN_ERROR",
770
+ logger_js_1.logger?.error('Failed to close MCP server', {
771
+ type: 'SERVER_SHUTDOWN_ERROR',
630
772
  error: error instanceof Error ? error.message : String(error),
631
773
  });
632
774
  }
633
775
  if (this.httpServer) {
634
776
  return new Promise((resolve) => {
635
- this.httpServer.close(() => {
636
- logger_js_1.logger.info("HTTP server closed");
777
+ this.httpServer?.close(() => {
778
+ logger_js_1.logger?.info('HTTP server closed');
637
779
  resolve();
638
780
  });
639
781
  });
@@ -643,3 +785,28 @@ class McpAbapAdtProxyServer {
643
785
  exports.McpAbapAdtProxyServer = McpAbapAdtProxyServer;
644
786
  // Export for use in bin script
645
787
  exports.default = McpAbapAdtProxyServer;
788
+ // Auto-start server when run directly (unless MCP_SKIP_AUTO_START is set)
789
+ if (process.env.MCP_SKIP_AUTO_START !== 'true') {
790
+ const server = new McpAbapAdtProxyServer();
791
+ server.run().catch((error) => {
792
+ logger_js_1.logger?.error('Fatal error while running MCP proxy server', {
793
+ type: 'SERVER_FATAL_ERROR',
794
+ error: error instanceof Error ? error.message : String(error),
795
+ });
796
+ // Always write fatal errors to stderr (even in non-verbose mode, as these are critical)
797
+ // But skip in test environment
798
+ const isTestEnv = process.env.NODE_ENV === 'test' ||
799
+ process.env.JEST_WORKER_ID !== undefined ||
800
+ typeof globalThis.jest !== 'undefined';
801
+ if (!isTestEnv) {
802
+ process.stderr.write(`[MCP Proxy] ✗ Fatal error: ${error instanceof Error ? error.message : String(error)}\n`);
803
+ }
804
+ // On Windows, add a small delay before exit to allow error message to be visible
805
+ if (process.platform === 'win32') {
806
+ setTimeout(() => process.exit(1), 100);
807
+ }
808
+ else {
809
+ process.exit(1);
810
+ }
811
+ });
812
+ }