@mcp-abap-adt/proxy 0.1.1

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 (38) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/LICENSE +22 -0
  3. package/README.md +234 -0
  4. package/bin/mcp-abap-adt-proxy.js +203 -0
  5. package/dist/index.d.ts +56 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +645 -0
  8. package/dist/lib/config.d.ts +33 -0
  9. package/dist/lib/config.d.ts.map +1 -0
  10. package/dist/lib/config.js +196 -0
  11. package/dist/lib/errorHandler.d.ts +69 -0
  12. package/dist/lib/errorHandler.d.ts.map +1 -0
  13. package/dist/lib/errorHandler.js +202 -0
  14. package/dist/lib/logger.d.ts +17 -0
  15. package/dist/lib/logger.d.ts.map +1 -0
  16. package/dist/lib/logger.js +43 -0
  17. package/dist/lib/stores.d.ts +18 -0
  18. package/dist/lib/stores.d.ts.map +1 -0
  19. package/dist/lib/stores.js +22 -0
  20. package/dist/lib/transportConfig.d.ts +18 -0
  21. package/dist/lib/transportConfig.d.ts.map +1 -0
  22. package/dist/lib/transportConfig.js +124 -0
  23. package/dist/proxy/cloudLlmHubProxy.d.ts +61 -0
  24. package/dist/proxy/cloudLlmHubProxy.d.ts.map +1 -0
  25. package/dist/proxy/cloudLlmHubProxy.js +380 -0
  26. package/dist/router/directCloudRouter.d.ts +32 -0
  27. package/dist/router/directCloudRouter.d.ts.map +1 -0
  28. package/dist/router/directCloudRouter.js +179 -0
  29. package/dist/router/headerAnalyzer.d.ts +48 -0
  30. package/dist/router/headerAnalyzer.d.ts.map +1 -0
  31. package/dist/router/headerAnalyzer.js +124 -0
  32. package/dist/router/localBasicRouter.d.ts +23 -0
  33. package/dist/router/localBasicRouter.d.ts.map +1 -0
  34. package/dist/router/localBasicRouter.js +140 -0
  35. package/dist/router/requestInterceptor.d.ts +32 -0
  36. package/dist/router/requestInterceptor.d.ts.map +1 -0
  37. package/dist/router/requestInterceptor.js +82 -0
  38. package/package.json +73 -0
package/dist/index.js ADDED
@@ -0,0 +1,645 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * MCP ABAP ADT Proxy Server
5
+ *
6
+ * Proxies local MCP requests to MCP servers with optional JWT authentication.
7
+ * Routes requests based on authentication headers:
8
+ * - x-btp-destination (or --btp): BTP Cloud authorization with JWT token
9
+ * - x-mcp-destination (or --mcp): SAP ABAP connection configuration
10
+ *
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)
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.McpAbapAdtProxyServer = void 0;
17
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
18
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
19
+ 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");
22
+ const config_js_1 = require("./lib/config.js");
23
+ const logger_js_1 = require("./lib/logger.js");
24
+ const requestInterceptor_js_1 = require("./router/requestInterceptor.js");
25
+ const headerAnalyzer_js_1 = require("./router/headerAnalyzer.js");
26
+ const cloudLlmHubProxy_js_1 = require("./proxy/cloudLlmHubProxy.js");
27
+ /**
28
+ * MCP ABAP ADT Proxy Server
29
+ */
30
+ class McpAbapAdtProxyServer {
31
+ server;
32
+ transportConfig;
33
+ config;
34
+ httpServer;
35
+ cloudLlmHubProxy;
36
+ constructor(transportConfig, configPath) {
37
+ this.transportConfig = transportConfig || (0, transportConfig_js_1.parseTransportConfig)();
38
+ this.config = (0, config_js_1.loadConfig)(configPath);
39
+ // Validate configuration
40
+ if (this.transportConfig.type === "streamable-http" || this.transportConfig.type === "sse") {
41
+ const validation = (0, config_js_1.validateConfig)(this.config);
42
+ if (!validation.valid) {
43
+ logger_js_1.logger.error("Configuration validation failed", {
44
+ type: "CONFIG_VALIDATION_ERROR",
45
+ errors: validation.errors,
46
+ });
47
+ throw new Error(`Configuration validation failed: ${validation.errors.join(", ")}`);
48
+ }
49
+ if (validation.warnings.length > 0) {
50
+ logger_js_1.logger.warn("Configuration validation warnings", {
51
+ type: "CONFIG_VALIDATION_WARNINGS",
52
+ warnings: validation.warnings,
53
+ });
54
+ }
55
+ }
56
+ this.server = new mcp_js_1.McpServer({
57
+ name: "@mcp-abap-adt/proxy",
58
+ version: "0.1.0",
59
+ }, {
60
+ capabilities: {
61
+ tools: {},
62
+ },
63
+ });
64
+ this.setupHandlers();
65
+ }
66
+ /**
67
+ * Setup MCP handlers
68
+ */
69
+ setupHandlers() {
70
+ // Tools are registered using server.registerTool()
71
+ // The server automatically handles initialize and tools/list requests
72
+ // For stdio, we'll intercept requests through server.server message handler
73
+ }
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
+ /**
146
+ * Run the proxy server
147
+ */
148
+ 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",
154
+ });
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).");
156
+ }
157
+ // For stdio, we need to register a proxy tool that forwards all requests
158
+ // 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
160
+ // Note: This requires the client to know about the proxy tool
161
+ // Alternatively, we could use a custom transport wrapper
162
+ // Simple stdio setup - MCP SDK will handle JSON-RPC protocol
163
+ // For tools/call requests, we need to intercept them
164
+ // Since MCP SDK doesn't provide a way to intercept all requests,
165
+ // we'll need to handle this differently
166
+ // For now, we'll just connect and let the SDK handle it
167
+ // The actual proxying will need to be done through registered tools
168
+ 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",
173
+ 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)",
178
+ });
179
+ return;
180
+ }
181
+ if (this.transportConfig.type === "streamable-http") {
182
+ await this.startHttpServer();
183
+ return;
184
+ }
185
+ if (this.transportConfig.type === "sse") {
186
+ await this.startSseServer();
187
+ return;
188
+ }
189
+ throw new Error(`Unsupported transport type: ${this.transportConfig.type}`);
190
+ }
191
+ /**
192
+ * Start HTTP server with request interception
193
+ */
194
+ async startHttpServer() {
195
+ 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
+ }
215
+ // Read request body
216
+ let body;
217
+ try {
218
+ const chunks = [];
219
+ for await (const chunk of req) {
220
+ chunks.push(chunk);
221
+ }
222
+ const bodyString = Buffer.concat(chunks).toString("utf-8");
223
+ body = JSON.parse(bodyString);
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
+ });
230
+ res.writeHead(400, { "Content-Type": "text/plain" });
231
+ res.end("Invalid request body");
232
+ return;
233
+ }
234
+ // Intercept and analyze request
235
+ // Pass config overrides (--btp, --mcp, and --mcp-url) to header analyzer
236
+ const configOverrides = {
237
+ btpDestination: this.config.btpDestination,
238
+ mcpDestination: this.config.mcpDestination,
239
+ mcpUrl: this.config.mcpUrl,
240
+ };
241
+ const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
242
+ // Check routing decision
243
+ if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
244
+ logger_js_1.logger.error("Routing decision failed", {
245
+ type: "ROUTING_DECISION_FAILED",
246
+ reason: intercepted.routingDecision.reason,
247
+ });
248
+ res.writeHead(400, { "Content-Type": "application/json" });
249
+ res.end(JSON.stringify({
250
+ jsonrpc: "2.0",
251
+ id: body?.id || null,
252
+ error: {
253
+ code: -32602,
254
+ message: intercepted.routingDecision.reason,
255
+ },
256
+ }));
257
+ return;
258
+ }
259
+ // Log proxy request
260
+ logger_js_1.logger.debug("Proxying request", {
261
+ type: "PROXY_REQUEST",
262
+ btpDestination: intercepted.routingDecision.btpDestination,
263
+ mcpDestination: intercepted.routingDecision.mcpDestination,
264
+ requiresSapConfig: (0, requestInterceptor_js_1.requiresSapConfig)(body),
265
+ });
266
+ // Handle proxy request - add JWT and forward to x-mcp-url
267
+ try {
268
+ await this.handleProxyRequest(intercepted, req, res);
269
+ }
270
+ catch (error) {
271
+ logger_js_1.logger.error("Failed to process request", {
272
+ type: "REQUEST_PROCESS_ERROR",
273
+ error: error instanceof Error ? error.message : String(error),
274
+ strategy: intercepted.routingDecision.strategy,
275
+ });
276
+ if (!res.headersSent) {
277
+ res.writeHead(500, { "Content-Type": "text/plain" });
278
+ res.end("Internal server error");
279
+ }
280
+ }
281
+ });
282
+ const port = httpConfig.port || 3001;
283
+ const host = httpConfig.host || "0.0.0.0";
284
+ 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
+ host,
290
+ port,
291
+ });
292
+ resolve();
293
+ });
294
+ this.httpServer.on("error", (error) => {
295
+ logger_js_1.logger.error("Failed to start HTTP server", {
296
+ type: "SERVER_START_ERROR",
297
+ error: error.message,
298
+ });
299
+ reject(error);
300
+ });
301
+ });
302
+ }
303
+ /**
304
+ * Handle proxy request - add JWT token and forward to MCP server
305
+ */
306
+ async handleProxyRequest(intercepted, req, res) {
307
+ 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",
312
+ btpDestination,
313
+ mcpDestination,
314
+ mcpUrl,
315
+ sessionId: intercepted.sessionId,
316
+ mode: btpDestination ? "BTP authentication" : "Local testing (no BTP authentication)",
317
+ });
318
+ try {
319
+ // 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);
324
+ }
325
+ // 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",
331
+ };
332
+ // Proxy request to cloud-llm-hub
333
+ const proxyResponse = await this.cloudLlmHubProxy.proxyRequest(mcpRequest, intercepted.routingDecision, intercepted.headers);
334
+ // Send response back to client
335
+ res.writeHead(200, { "Content-Type": "application/json" });
336
+ res.end(JSON.stringify(proxyResponse));
337
+ logger_js_1.logger.debug("Cloud-llm-hub proxy request completed", {
338
+ type: "CLOUD_LLM_HUB_PROXY_COMPLETED",
339
+ hasResult: !!proxyResponse.result,
340
+ hasError: !!proxyResponse.error,
341
+ });
342
+ }
343
+ 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),
347
+ stack: error instanceof Error ? error.stack : undefined,
348
+ });
349
+ if (!res.headersSent) {
350
+ res.writeHead(500, { "Content-Type": "application/json" });
351
+ res.end(JSON.stringify({
352
+ jsonrpc: "2.0",
353
+ id: intercepted.body?.id || null,
354
+ error: {
355
+ code: -32000,
356
+ message: error instanceof Error ? error.message : "Unknown error",
357
+ },
358
+ }));
359
+ }
360
+ }
361
+ }
362
+ /**
363
+ * Start SSE server
364
+ */
365
+ async startSseServer() {
366
+ const sseConfig = this.transportConfig;
367
+ const streamPathMap = new Map([
368
+ ["/", "/messages"],
369
+ ["/mcp/events", "/mcp/messages"],
370
+ ["/sse", "/messages"],
371
+ ]);
372
+ const streamPaths = Array.from(streamPathMap.keys());
373
+ 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) => {
377
+ // SSE: Always restrict to local connections only
378
+ const remoteAddress = req.socket.remoteAddress;
379
+ const isLocal = remoteAddress === "127.0.0.1" ||
380
+ remoteAddress === "::1" ||
381
+ remoteAddress === "localhost" ||
382
+ (remoteAddress && remoteAddress.startsWith("127."));
383
+ if (!isLocal) {
384
+ logger_js_1.logger.warn("SSE: Non-local connection rejected", {
385
+ type: "SSE_NON_LOCAL_REJECTED",
386
+ remoteAddress,
387
+ });
388
+ res.writeHead(403, { "Content-Type": "text/plain" });
389
+ res.end("Forbidden: SSE transport only accepts local connections");
390
+ return;
391
+ }
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("/")) {
395
+ pathname = pathname.slice(0, -1);
396
+ }
397
+ // Apply config overrides (--btp, --mcp, and --mcp-url) to headers if not present
398
+ const headers = {};
399
+ for (const [key, value] of Object.entries(req.headers)) {
400
+ if (value) {
401
+ headers[key] = Array.isArray(value) ? value[0] : value;
402
+ }
403
+ }
404
+ // 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;
413
+ }
414
+ logger_js_1.logger.debug("SSE request received", {
415
+ type: "SSE_HTTP_REQUEST",
416
+ method: req.method,
417
+ pathname,
418
+ originalUrl: req.url,
419
+ });
420
+ // 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",
425
+ pathname,
426
+ postEndpoint,
427
+ });
428
+ // Create new McpServer instance for this session
429
+ const sessionServer = new mcp_js_1.McpServer({
430
+ name: "@mcp-abap-adt/proxy",
431
+ version: "0.1.0",
432
+ }, {
433
+ capabilities: {
434
+ tools: {},
435
+ },
436
+ });
437
+ // Create SSE transport
438
+ const transport = new sse_js_1.SSEServerTransport(postEndpoint, res, {
439
+ allowedHosts: sseConfig.allowedHosts,
440
+ allowedOrigins: sseConfig.allowedOrigins,
441
+ enableDnsRebindingProtection: sseConfig.enableDnsRebindingProtection,
442
+ });
443
+ const sessionId = transport.sessionId;
444
+ logger_js_1.logger.info("New SSE session created", {
445
+ type: "SSE_SESSION_CREATED",
446
+ sessionId,
447
+ pathname,
448
+ });
449
+ // Connect transport to server
450
+ try {
451
+ await sessionServer.server.connect(transport);
452
+ logger_js_1.logger.info("SSE transport connected", {
453
+ type: "SSE_CONNECTION_READY",
454
+ sessionId,
455
+ pathname,
456
+ postEndpoint,
457
+ });
458
+ }
459
+ catch (error) {
460
+ logger_js_1.logger.error("Failed to connect SSE transport", {
461
+ type: "SSE_CONNECT_ERROR",
462
+ error: error instanceof Error ? error.message : String(error),
463
+ sessionId,
464
+ });
465
+ if (!res.headersSent) {
466
+ res.writeHead(500).end("Internal Server Error");
467
+ }
468
+ else {
469
+ res.end();
470
+ }
471
+ return;
472
+ }
473
+ // Cleanup on connection close
474
+ res.on("close", () => {
475
+ logger_js_1.logger.info("SSE connection closed", {
476
+ type: "SSE_CONNECTION_CLOSED",
477
+ sessionId,
478
+ pathname,
479
+ });
480
+ sessionServer.server.close();
481
+ });
482
+ transport.onerror = (error) => {
483
+ logger_js_1.logger.error("SSE transport error", {
484
+ type: "SSE_TRANSPORT_ERROR",
485
+ error: error instanceof Error ? error.message : String(error),
486
+ sessionId,
487
+ });
488
+ };
489
+ return;
490
+ }
491
+ // POST /messages or /mcp/messages - handle client messages
492
+ if (req.method === "POST" && postPathSet.has(pathname)) {
493
+ // Extract sessionId from query string or header
494
+ let sessionId;
495
+ if (requestUrl) {
496
+ sessionId = requestUrl.searchParams.get("sessionId") || undefined;
497
+ }
498
+ if (!sessionId) {
499
+ sessionId = req.headers["x-session-id"];
500
+ }
501
+ logger_js_1.logger.debug("SSE POST request received", {
502
+ type: "SSE_POST_REQUEST",
503
+ sessionId,
504
+ pathname,
505
+ });
506
+ if (!sessionId) {
507
+ logger_js_1.logger.error("Missing sessionId in SSE POST request", {
508
+ type: "SSE_MISSING_SESSION_ID",
509
+ pathname,
510
+ });
511
+ res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({
512
+ jsonrpc: "2.0",
513
+ error: {
514
+ code: -32000,
515
+ message: "Missing sessionId",
516
+ },
517
+ id: null,
518
+ }));
519
+ return;
520
+ }
521
+ // Read request body
522
+ let body;
523
+ try {
524
+ const chunks = [];
525
+ for await (const chunk of req) {
526
+ chunks.push(chunk);
527
+ }
528
+ if (chunks.length > 0) {
529
+ const bodyString = Buffer.concat(chunks).toString("utf-8");
530
+ body = JSON.parse(bodyString);
531
+ }
532
+ }
533
+ catch (error) {
534
+ logger_js_1.logger.error("Failed to parse SSE POST request body", {
535
+ type: "SSE_POST_PARSE_ERROR",
536
+ error: error instanceof Error ? error.message : String(error),
537
+ });
538
+ res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({
539
+ jsonrpc: "2.0",
540
+ error: {
541
+ code: -32700,
542
+ message: "Parse error",
543
+ },
544
+ id: null,
545
+ }));
546
+ return;
547
+ }
548
+ // Intercept and analyze request with config overrides
549
+ const configOverrides = {
550
+ btpDestination: this.config.btpDestination,
551
+ mcpDestination: this.config.mcpDestination,
552
+ mcpUrl: this.config.mcpUrl,
553
+ };
554
+ const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
555
+ // Check routing decision
556
+ 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",
559
+ reason: intercepted.routingDecision.reason,
560
+ });
561
+ res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({
562
+ jsonrpc: "2.0",
563
+ id: body?.id || null,
564
+ error: {
565
+ code: -32602,
566
+ message: intercepted.routingDecision.reason,
567
+ },
568
+ }));
569
+ return;
570
+ }
571
+ // Handle proxy request
572
+ try {
573
+ await this.handleProxyRequest(intercepted, req, res);
574
+ }
575
+ catch (error) {
576
+ logger_js_1.logger.error("Failed to process SSE POST request", {
577
+ type: "SSE_POST_PROCESS_ERROR",
578
+ error: error instanceof Error ? error.message : String(error),
579
+ });
580
+ if (!res.headersSent) {
581
+ res.writeHead(500, { "Content-Type": "application/json" });
582
+ res.end(JSON.stringify({
583
+ jsonrpc: "2.0",
584
+ id: body?.id || null,
585
+ error: {
586
+ code: -32000,
587
+ message: error instanceof Error ? error.message : "Unknown error",
588
+ },
589
+ }));
590
+ }
591
+ }
592
+ return;
593
+ }
594
+ // Unknown path or method
595
+ res.writeHead(404, { "Content-Type": "text/plain" });
596
+ res.end("Not found");
597
+ });
598
+ const port = sseConfig.port || 3002;
599
+ const host = sseConfig.host || "0.0.0.0";
600
+ return new Promise((resolve, reject) => {
601
+ httpServer.listen(port, host, () => {
602
+ logger_js_1.logger.info("MCP Proxy Server started (SSE transport)", {
603
+ type: "SERVER_STARTED",
604
+ transport: "sse",
605
+ host,
606
+ port,
607
+ });
608
+ this.httpServer = httpServer;
609
+ resolve();
610
+ });
611
+ httpServer.on("error", (error) => {
612
+ logger_js_1.logger.error("Failed to start SSE server", {
613
+ type: "SERVER_START_ERROR",
614
+ error: error.message,
615
+ });
616
+ reject(error);
617
+ });
618
+ });
619
+ }
620
+ /**
621
+ * Shutdown server
622
+ */
623
+ async shutdown() {
624
+ try {
625
+ await this.server.close();
626
+ }
627
+ catch (error) {
628
+ logger_js_1.logger.error("Failed to close MCP server", {
629
+ type: "SERVER_SHUTDOWN_ERROR",
630
+ error: error instanceof Error ? error.message : String(error),
631
+ });
632
+ }
633
+ if (this.httpServer) {
634
+ return new Promise((resolve) => {
635
+ this.httpServer.close(() => {
636
+ logger_js_1.logger.info("HTTP server closed");
637
+ resolve();
638
+ });
639
+ });
640
+ }
641
+ }
642
+ }
643
+ exports.McpAbapAdtProxyServer = McpAbapAdtProxyServer;
644
+ // Export for use in bin script
645
+ exports.default = McpAbapAdtProxyServer;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Configuration management for MCP ABAP ADT Proxy
3
+ */
4
+ export interface ProxyConfig {
5
+ cloudLlmHubUrl: string;
6
+ httpPort: number;
7
+ ssePort: number;
8
+ httpHost: string;
9
+ sseHost: string;
10
+ logLevel: string;
11
+ btpDestination?: string;
12
+ mcpDestination?: string;
13
+ mcpUrl?: string;
14
+ unsafe?: boolean;
15
+ maxRetries?: number;
16
+ retryDelay?: number;
17
+ requestTimeout?: number;
18
+ circuitBreakerThreshold?: number;
19
+ circuitBreakerTimeout?: number;
20
+ }
21
+ /**
22
+ * Load configuration from file if exists, otherwise from environment variables
23
+ */
24
+ export declare function loadConfig(configPath?: string): ProxyConfig;
25
+ /**
26
+ * Validate configuration
27
+ */
28
+ export declare function validateConfig(config: ProxyConfig): {
29
+ valid: boolean;
30
+ errors: string[];
31
+ warnings: string[];
32
+ };
33
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IAEjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAoC3D;AA+ED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA6C5G"}