@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
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * Direct Cloud Router - Routes requests directly to cloud ABAP systems
4
+ *
5
+ * For requests with x-sap-destination (not "sk"), routes directly to cloud ABAP
6
+ * using the same approach as mcp-abap-adt
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createDirectCloudConfig = createDirectCloudConfig;
10
+ exports.getDirectCloudConnection = getDirectCloudConnection;
11
+ const connection_1 = require("@mcp-abap-adt/connection");
12
+ const logger_js_1 = require("../lib/logger.js");
13
+ /**
14
+ * Convert validated auth config to direct cloud config
15
+ */
16
+ function createDirectCloudConfig(routingDecision, headers) {
17
+ const validatedConfig = routingDecision.validationResult?.config;
18
+ if (!validatedConfig) {
19
+ logger_js_1.logger.warn("No validated config available for direct cloud routing", {
20
+ type: "DIRECT_CLOUD_CONFIG_ERROR",
21
+ strategy: routingDecision.strategy,
22
+ });
23
+ return null;
24
+ }
25
+ // Extract SAP URL
26
+ let sapUrl = validatedConfig.sapUrl;
27
+ const destination = routingDecision.btpDestination || routingDecision.mcpDestination;
28
+ if (!sapUrl && destination) {
29
+ // For destination-based auth, URL comes from destination service key
30
+ // We'll need to load it from AuthBroker
31
+ // For now, return null and let the caller handle it
32
+ logger_js_1.logger.debug("SAP URL not in validated config, will load from destination", {
33
+ destination: destination,
34
+ });
35
+ }
36
+ // Convert xsuaa to jwt (they're the same for connection purposes)
37
+ const authType = validatedConfig.authType === "xsuaa" ? "jwt" : validatedConfig.authType;
38
+ const config = {
39
+ sapUrl: sapUrl || "", // Will be loaded from destination if needed
40
+ destination: validatedConfig.destination || destination,
41
+ authType: authType,
42
+ sapClient: validatedConfig.sapClient,
43
+ };
44
+ // Add JWT-specific fields
45
+ if (validatedConfig.authType === "jwt" || validatedConfig.authType === "xsuaa") {
46
+ config.jwtToken = validatedConfig.jwtToken;
47
+ config.refreshToken = validatedConfig.refreshToken;
48
+ config.uaaUrl = validatedConfig.uaaUrl;
49
+ config.uaaClientId = validatedConfig.uaaClientId;
50
+ config.uaaClientSecret = validatedConfig.uaaClientSecret;
51
+ }
52
+ // Add basic auth fields
53
+ if (validatedConfig.authType === "basic") {
54
+ config.username = validatedConfig.username;
55
+ config.password = validatedConfig.password;
56
+ }
57
+ return config;
58
+ }
59
+ /**
60
+ * Connection cache for direct cloud connections
61
+ */
62
+ const connectionCache = new Map();
63
+ /**
64
+ * Generate cache key for connection
65
+ */
66
+ function generateConnectionCacheKey(sessionId, config) {
67
+ const crypto = require("crypto");
68
+ const hash = crypto.createHash("sha256");
69
+ hash.update(sessionId);
70
+ hash.update(config.sapUrl);
71
+ hash.update(config.destination || "");
72
+ hash.update(config.authType);
73
+ hash.update(config.sapClient || "");
74
+ return hash.digest("hex");
75
+ }
76
+ /**
77
+ * Clean up old connections from cache
78
+ */
79
+ function cleanupConnectionCache() {
80
+ const now = new Date();
81
+ const maxAge = 60 * 60 * 1000; // 1 hour
82
+ for (const [key, entry] of connectionCache.entries()) {
83
+ const age = now.getTime() - entry.lastUsed.getTime();
84
+ if (age > maxAge) {
85
+ logger_js_1.logger.debug("Cleaning up old connection cache entry", {
86
+ type: "CONNECTION_CACHE_CLEANUP",
87
+ key: key.substring(0, 16),
88
+ });
89
+ connectionCache.delete(key);
90
+ }
91
+ }
92
+ }
93
+ /**
94
+ * Get or create ABAP connection for direct cloud routing
95
+ */
96
+ async function getDirectCloudConnection(sessionId, config, authBroker) {
97
+ // Clean up old entries periodically
98
+ if (connectionCache.size > 100) {
99
+ cleanupConnectionCache();
100
+ }
101
+ const cacheKey = generateConnectionCacheKey(sessionId, config);
102
+ let entry = connectionCache.get(cacheKey);
103
+ // If we have a destination and no SAP URL, load it from AuthBroker
104
+ if (config.destination && !config.sapUrl && authBroker) {
105
+ try {
106
+ // Get token from AuthBroker (this will also provide the SAP URL from destination)
107
+ const token = await authBroker.getToken(config.destination);
108
+ config.jwtToken = token;
109
+ // Load destination config to get SAP URL from AuthBroker
110
+ // AuthBroker stores session data, we can access it through the broker
111
+ // For now, we'll rely on the token being valid and the URL being set from destination
112
+ // The actual URL will be loaded when we create the connection
113
+ }
114
+ catch (error) {
115
+ logger_js_1.logger.error("Failed to load destination config from AuthBroker", {
116
+ type: "AUTH_BROKER_LOAD_ERROR",
117
+ destination: config.destination,
118
+ error: error instanceof Error ? error.message : String(error),
119
+ });
120
+ throw error;
121
+ }
122
+ }
123
+ if (!entry || entry.configSignature !== cacheKey) {
124
+ logger_js_1.logger.debug("Creating new direct cloud connection", {
125
+ type: "DIRECT_CLOUD_CONNECTION_CREATE",
126
+ sessionId: sessionId.substring(0, 8),
127
+ destination: config.destination,
128
+ sapUrl: config.sapUrl,
129
+ });
130
+ // Dispose old connection if exists
131
+ if (entry) {
132
+ // Connection disposal will be handled by connection package
133
+ }
134
+ // Create SAP config for connection
135
+ const sapConfig = {
136
+ url: config.sapUrl,
137
+ authType: config.authType,
138
+ jwtToken: config.jwtToken,
139
+ refreshToken: config.refreshToken,
140
+ username: config.username,
141
+ password: config.password,
142
+ sapClient: config.sapClient,
143
+ uaaUrl: config.uaaUrl,
144
+ uaaClientId: config.uaaClientId,
145
+ uaaClientSecret: config.uaaClientSecret,
146
+ };
147
+ // Create connection
148
+ const connectionSessionId = `mcp-proxy-direct-${sessionId}`;
149
+ const sessionStorage = new connection_1.FileSessionStorage();
150
+ const connection = (0, connection_1.createAbapConnection)(sapConfig, logger_js_1.logger, // Use our logger
151
+ sessionStorage, connectionSessionId);
152
+ // Connect
153
+ try {
154
+ await connection.connect();
155
+ }
156
+ catch (error) {
157
+ logger_js_1.logger.error("Failed to connect to cloud ABAP", {
158
+ type: "DIRECT_CLOUD_CONNECTION_ERROR",
159
+ error: error instanceof Error ? error.message : String(error),
160
+ destination: config.destination,
161
+ });
162
+ throw error;
163
+ }
164
+ entry = {
165
+ connection,
166
+ configSignature: cacheKey,
167
+ lastUsed: new Date(),
168
+ };
169
+ connectionCache.set(cacheKey, entry);
170
+ }
171
+ else {
172
+ entry.lastUsed = new Date();
173
+ logger_js_1.logger.debug("Reusing cached direct cloud connection", {
174
+ type: "DIRECT_CLOUD_CONNECTION_REUSE",
175
+ sessionId: sessionId.substring(0, 8),
176
+ });
177
+ }
178
+ return entry.connection;
179
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Header Analyzer - Analyzes HTTP headers to determine routing strategy
3
+ */
4
+ import { IncomingHttpHeaders } from "http";
5
+ import { HeaderValidationResult } from "@mcp-abap-adt/header-validator";
6
+ export declare enum RoutingStrategy {
7
+ /** Proxy request with JWT authentication */
8
+ PROXY = "proxy",
9
+ /** Unknown/unsupported - should not route */
10
+ UNKNOWN = "unknown"
11
+ }
12
+ export interface RoutingDecision {
13
+ strategy: RoutingStrategy;
14
+ btpDestination?: string;
15
+ mcpDestination?: string;
16
+ mcpUrl?: string;
17
+ reason: string;
18
+ validationResult?: HeaderValidationResult;
19
+ }
20
+ /**
21
+ * Analyze headers and extract routing information
22
+ * Proxy needs either:
23
+ * - x-btp-destination (or --btp): destination for BTP Cloud authorization token (Authorization: Bearer) and MCP server URL
24
+ * - x-mcp-destination (or --mcp): destination for SAP ABAP connection (SAP headers) - can be used without BTP for local testing
25
+ *
26
+ * Also supports:
27
+ * - x-mcp-url (or --mcp-url): direct MCP server URL (used when no BTP destination is provided, for local testing)
28
+ *
29
+ * Command-line overrides:
30
+ * - --btp=<destination>: overrides x-btp-destination header
31
+ * - --mcp=<destination>: overrides x-mcp-destination header
32
+ * - --mcp-url=<url>: overrides x-mcp-url header (for local testing without BTP)
33
+ *
34
+ * MCP server URL is obtained from:
35
+ * - x-mcp-url header or --mcp-url parameter (if provided) - takes precedence
36
+ * - service key for x-btp-destination (via auth-broker) if BTP destination is present
37
+ * - service key for x-mcp-destination (via auth-broker) if only MCP destination is present
38
+ */
39
+ export declare function analyzeHeaders(headers: IncomingHttpHeaders, configOverrides?: {
40
+ btpDestination?: string;
41
+ mcpDestination?: string;
42
+ mcpUrl?: string;
43
+ }): RoutingDecision;
44
+ /**
45
+ * Check if headers indicate a request that should be proxied
46
+ */
47
+ export declare function shouldProxy(headers: IncomingHttpHeaders): boolean;
48
+ //# sourceMappingURL=headerAnalyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headerAnalyzer.d.ts","sourceRoot":"","sources":["../../src/router/headerAnalyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAA2C,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAGjH,oBAAY,eAAe;IACzB,4CAA4C;IAC5C,KAAK,UAAU;IACf,6CAA6C;IAC7C,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,eAAe,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,mBAAmB,EAC5B,eAAe,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACtF,eAAe,CAyFjB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAGjE"}
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /**
3
+ * Header Analyzer - Analyzes HTTP headers to determine routing strategy
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RoutingStrategy = void 0;
7
+ exports.analyzeHeaders = analyzeHeaders;
8
+ exports.shouldProxy = shouldProxy;
9
+ const header_validator_1 = require("@mcp-abap-adt/header-validator");
10
+ const logger_js_1 = require("../lib/logger.js");
11
+ var RoutingStrategy;
12
+ (function (RoutingStrategy) {
13
+ /** Proxy request with JWT authentication */
14
+ RoutingStrategy["PROXY"] = "proxy";
15
+ /** Unknown/unsupported - should not route */
16
+ RoutingStrategy["UNKNOWN"] = "unknown";
17
+ })(RoutingStrategy || (exports.RoutingStrategy = RoutingStrategy = {}));
18
+ /**
19
+ * Analyze headers and extract routing information
20
+ * Proxy needs either:
21
+ * - x-btp-destination (or --btp): destination for BTP Cloud authorization token (Authorization: Bearer) and MCP server URL
22
+ * - x-mcp-destination (or --mcp): destination for SAP ABAP connection (SAP headers) - can be used without BTP for local testing
23
+ *
24
+ * Also supports:
25
+ * - x-mcp-url (or --mcp-url): direct MCP server URL (used when no BTP destination is provided, for local testing)
26
+ *
27
+ * Command-line overrides:
28
+ * - --btp=<destination>: overrides x-btp-destination header
29
+ * - --mcp=<destination>: overrides x-mcp-destination header
30
+ * - --mcp-url=<url>: overrides x-mcp-url header (for local testing without BTP)
31
+ *
32
+ * MCP server URL is obtained from:
33
+ * - x-mcp-url header or --mcp-url parameter (if provided) - takes precedence
34
+ * - service key for x-btp-destination (via auth-broker) if BTP destination is present
35
+ * - service key for x-mcp-destination (via auth-broker) if only MCP destination is present
36
+ */
37
+ function analyzeHeaders(headers, configOverrides) {
38
+ // Validate headers using header-validator
39
+ const validationResult = (0, header_validator_1.validateAuthHeaders)(headers);
40
+ const validatedConfig = validationResult.config;
41
+ // Helper function to extract string value from header (handles arrays)
42
+ const getHeaderValue = (headerValue) => {
43
+ if (!headerValue)
44
+ return undefined;
45
+ if (Array.isArray(headerValue)) {
46
+ return headerValue[0]?.trim();
47
+ }
48
+ return typeof headerValue === "string" ? headerValue.trim() : undefined;
49
+ };
50
+ // Extract authorization destination for BTP Cloud (x-btp-destination)
51
+ // Command-line parameter --btp takes precedence over header
52
+ const btpDestinationHeader = getHeaderValue(headers["x-btp-destination"]);
53
+ const extractedBtpDestination = configOverrides?.btpDestination
54
+ ? configOverrides.btpDestination
55
+ : btpDestinationHeader;
56
+ // Extract destination for SAP ABAP connection (x-mcp-destination)
57
+ // Command-line parameter --mcp takes precedence over header
58
+ const mcpDestinationHeader = getHeaderValue(headers["x-mcp-destination"]);
59
+ const extractedMcpDestination = configOverrides?.mcpDestination
60
+ ? configOverrides.mcpDestination
61
+ : mcpDestinationHeader;
62
+ // Extract direct MCP server URL (x-mcp-url)
63
+ // Command-line parameter --mcp-url takes precedence over header
64
+ const mcpUrlHeader = getHeaderValue(headers["x-mcp-url"]);
65
+ const extractedMcpUrl = configOverrides?.mcpUrl
66
+ ? configOverrides.mcpUrl
67
+ : mcpUrlHeader;
68
+ // Allow proxying if either:
69
+ // 1. BTP destination is present, OR
70
+ // 2. MCP destination is present, OR
71
+ // 3. MCP URL is provided (for local testing without BTP)
72
+ // This enables local testing without BTP authentication
73
+ if (!extractedBtpDestination && !extractedMcpDestination && !extractedMcpUrl) {
74
+ logger_js_1.logger.warn("Neither x-btp-destination/x-mcp-destination/x-mcp-url header nor --btp/--mcp/--mcp-url parameter is provided", {
75
+ headers: Object.keys(headers).filter(k => k.toLowerCase().startsWith("x-")),
76
+ hasBtpConfigOverride: !!configOverrides?.btpDestination,
77
+ hasMcpConfigOverride: !!configOverrides?.mcpDestination,
78
+ hasMcpUrlConfigOverride: !!configOverrides?.mcpUrl,
79
+ });
80
+ return {
81
+ strategy: RoutingStrategy.UNKNOWN,
82
+ reason: "Either x-btp-destination/--btp, x-mcp-destination/--mcp, or x-mcp-url/--mcp-url parameter is required for proxying",
83
+ validationResult,
84
+ };
85
+ }
86
+ // If only MCP URL is provided without destination, we still need either mcpDestination or btpDestination
87
+ // unless mcpUrl is a full URL that can be used directly
88
+ if (!extractedBtpDestination && !extractedMcpDestination && extractedMcpUrl) {
89
+ // This is OK - we can use mcpUrl directly for local testing
90
+ logger_js_1.logger.debug("Using MCP URL directly for local testing (no destination required)", {
91
+ type: "MCP_URL_DIRECT",
92
+ mcpUrl: extractedMcpUrl,
93
+ });
94
+ }
95
+ // If x-btp-destination is present, we can proxy with BTP authentication (URL will be obtained from service key)
96
+ // If only x-mcp-destination is present, we can proxy without BTP authentication (for local testing)
97
+ // If x-mcp-url is provided, we can use it directly (for local testing)
98
+ logger_js_1.logger.debug("Routing decision: PROXY", {
99
+ btpDestination: extractedBtpDestination,
100
+ mcpDestination: extractedMcpDestination,
101
+ mcpUrl: extractedMcpUrl || "not provided",
102
+ priority: validatedConfig?.priority,
103
+ });
104
+ const reason = extractedBtpDestination
105
+ ? `Proxying to MCP server from BTP destination "${extractedBtpDestination}"${extractedMcpDestination ? ` with MCP destination "${extractedMcpDestination}"` : ''}`
106
+ : extractedMcpUrl
107
+ ? `Proxying to MCP server at "${extractedMcpUrl}" (no BTP authentication - local testing mode)${extractedMcpDestination ? ` with MCP destination "${extractedMcpDestination}"` : ''}`
108
+ : `Proxying to MCP server with MCP destination "${extractedMcpDestination}" (no BTP authentication - local testing mode)`;
109
+ return {
110
+ strategy: RoutingStrategy.PROXY,
111
+ btpDestination: extractedBtpDestination, // Optional - only used if provided
112
+ mcpDestination: extractedMcpDestination, // Optional - only used if provided
113
+ mcpUrl: extractedMcpUrl, // Optional - direct URL if provided (takes precedence)
114
+ reason,
115
+ validationResult,
116
+ };
117
+ }
118
+ /**
119
+ * Check if headers indicate a request that should be proxied
120
+ */
121
+ function shouldProxy(headers) {
122
+ const decision = analyzeHeaders(headers);
123
+ return decision.strategy === RoutingStrategy.PROXY;
124
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Local Basic Router - Handles basic authentication requests locally
3
+ *
4
+ * For requests with x-sap-auth-type: "basic", handles locally without proxying
5
+ */
6
+ import { IncomingHttpHeaders } from "http";
7
+ import { AbapConnection } from "@mcp-abap-adt/connection";
8
+ import { RoutingDecision } from "./headerAnalyzer.js";
9
+ export interface LocalBasicConfig {
10
+ sapUrl: string;
11
+ username: string;
12
+ password: string;
13
+ sapClient?: string;
14
+ }
15
+ /**
16
+ * Convert validated auth config to local basic config
17
+ */
18
+ export declare function createLocalBasicConfig(routingDecision: RoutingDecision, headers: IncomingHttpHeaders): LocalBasicConfig | null;
19
+ /**
20
+ * Get or create ABAP connection for local basic auth
21
+ */
22
+ export declare function getLocalBasicConnection(sessionId: string, config: LocalBasicConfig): Promise<AbapConnection>;
23
+ //# sourceMappingURL=localBasicRouter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localBasicRouter.d.ts","sourceRoot":"","sources":["../../src/router/localBasicRouter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAwB,cAAc,EAAsB,MAAM,0BAA0B,CAAC;AAEpG,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,mBAAmB,GAC3B,gBAAgB,GAAG,IAAI,CAkCzB;AA2CD;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,cAAc,CAAC,CAoEzB"}
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ /**
3
+ * Local Basic Router - Handles basic authentication requests locally
4
+ *
5
+ * For requests with x-sap-auth-type: "basic", handles locally without proxying
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.createLocalBasicConfig = createLocalBasicConfig;
9
+ exports.getLocalBasicConnection = getLocalBasicConnection;
10
+ const connection_1 = require("@mcp-abap-adt/connection");
11
+ const logger_js_1 = require("../lib/logger.js");
12
+ /**
13
+ * Convert validated auth config to local basic config
14
+ */
15
+ function createLocalBasicConfig(routingDecision, headers) {
16
+ const validatedConfig = routingDecision.validationResult?.config;
17
+ if (!validatedConfig) {
18
+ logger_js_1.logger.warn("No validated config available for local basic routing", {
19
+ type: "LOCAL_BASIC_CONFIG_ERROR",
20
+ strategy: routingDecision.strategy,
21
+ });
22
+ return null;
23
+ }
24
+ if (validatedConfig.authType !== "basic") {
25
+ logger_js_1.logger.warn("Config is not basic auth type", {
26
+ type: "LOCAL_BASIC_CONFIG_TYPE_ERROR",
27
+ authType: validatedConfig.authType,
28
+ });
29
+ return null;
30
+ }
31
+ if (!validatedConfig.sapUrl || !validatedConfig.username || !validatedConfig.password) {
32
+ logger_js_1.logger.warn("Missing required fields for basic auth", {
33
+ type: "LOCAL_BASIC_CONFIG_MISSING",
34
+ hasUrl: !!validatedConfig.sapUrl,
35
+ hasUsername: !!validatedConfig.username,
36
+ hasPassword: !!validatedConfig.password,
37
+ });
38
+ return null;
39
+ }
40
+ return {
41
+ sapUrl: validatedConfig.sapUrl,
42
+ username: validatedConfig.username,
43
+ password: validatedConfig.password,
44
+ sapClient: validatedConfig.sapClient,
45
+ };
46
+ }
47
+ /**
48
+ * Connection cache for local basic connections
49
+ */
50
+ const connectionCache = new Map();
51
+ /**
52
+ * Generate cache key for connection
53
+ */
54
+ function generateConnectionCacheKey(sessionId, config) {
55
+ const crypto = require("crypto");
56
+ const hash = crypto.createHash("sha256");
57
+ hash.update(sessionId);
58
+ hash.update(config.sapUrl);
59
+ hash.update(config.username);
60
+ hash.update(config.sapClient || "");
61
+ return hash.digest("hex");
62
+ }
63
+ /**
64
+ * Clean up old connections from cache
65
+ */
66
+ function cleanupConnectionCache() {
67
+ const now = new Date();
68
+ const maxAge = 60 * 60 * 1000; // 1 hour
69
+ for (const [key, entry] of connectionCache.entries()) {
70
+ const age = now.getTime() - entry.lastUsed.getTime();
71
+ if (age > maxAge) {
72
+ logger_js_1.logger.debug("Cleaning up old local basic connection cache entry", {
73
+ type: "LOCAL_BASIC_CACHE_CLEANUP",
74
+ key: key.substring(0, 16),
75
+ });
76
+ connectionCache.delete(key);
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Get or create ABAP connection for local basic auth
82
+ */
83
+ async function getLocalBasicConnection(sessionId, config) {
84
+ // Clean up old entries periodically
85
+ if (connectionCache.size > 100) {
86
+ cleanupConnectionCache();
87
+ }
88
+ const cacheKey = generateConnectionCacheKey(sessionId, config);
89
+ let entry = connectionCache.get(cacheKey);
90
+ if (!entry || entry.configSignature !== cacheKey) {
91
+ logger_js_1.logger.debug("Creating new local basic connection", {
92
+ type: "LOCAL_BASIC_CONNECTION_CREATE",
93
+ sessionId: sessionId.substring(0, 8),
94
+ sapUrl: config.sapUrl,
95
+ });
96
+ // Dispose old connection if exists
97
+ if (entry) {
98
+ // Connection disposal will be handled by connection package
99
+ }
100
+ // Create SAP config for connection
101
+ const sapConfig = {
102
+ url: config.sapUrl,
103
+ authType: "basic",
104
+ username: config.username,
105
+ password: config.password,
106
+ client: config.sapClient,
107
+ };
108
+ // Create connection
109
+ const connectionSessionId = `mcp-proxy-basic-${sessionId}`;
110
+ const sessionStorage = new connection_1.FileSessionStorage();
111
+ const connection = (0, connection_1.createAbapConnection)(sapConfig, logger_js_1.logger, // Use our logger
112
+ sessionStorage, connectionSessionId);
113
+ // Connect
114
+ try {
115
+ await connection.connect();
116
+ }
117
+ catch (error) {
118
+ logger_js_1.logger.error("Failed to connect to ABAP with basic auth", {
119
+ type: "LOCAL_BASIC_CONNECTION_ERROR",
120
+ error: error instanceof Error ? error.message : String(error),
121
+ sapUrl: config.sapUrl,
122
+ });
123
+ throw error;
124
+ }
125
+ entry = {
126
+ connection,
127
+ configSignature: cacheKey,
128
+ lastUsed: new Date(),
129
+ };
130
+ connectionCache.set(cacheKey, entry);
131
+ }
132
+ else {
133
+ entry.lastUsed = new Date();
134
+ logger_js_1.logger.debug("Reusing cached local basic connection", {
135
+ type: "LOCAL_BASIC_CONNECTION_REUSE",
136
+ sessionId: sessionId.substring(0, 8),
137
+ });
138
+ }
139
+ return entry.connection;
140
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Request Interceptor - Intercepts and analyzes MCP requests
3
+ */
4
+ import { IncomingMessage } from "http";
5
+ import { RoutingDecision } from "./headerAnalyzer.js";
6
+ export interface InterceptedRequest {
7
+ method: string;
8
+ url: string;
9
+ headers: Record<string, string | string[] | undefined>;
10
+ body?: any;
11
+ routingDecision: RoutingDecision;
12
+ sessionId?: string;
13
+ clientId?: string;
14
+ }
15
+ /**
16
+ * Intercept and analyze incoming HTTP request
17
+ */
18
+ export declare function interceptRequest(req: IncomingMessage, body?: any, configOverrides?: {
19
+ btpDestination?: string;
20
+ mcpDestination?: string;
21
+ mcpUrl?: string;
22
+ }): InterceptedRequest;
23
+ /**
24
+ * Check if request requires SAP configuration
25
+ * Only tools/call requires SAP config - all other methods don't
26
+ */
27
+ export declare function requiresSapConfig(body: any): boolean;
28
+ /**
29
+ * Sanitize headers for logging (remove sensitive data)
30
+ */
31
+ export declare function sanitizeHeadersForLogging(headers: Record<string, string | string[] | undefined>): Record<string, string>;
32
+ //# sourceMappingURL=requestInterceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestInterceptor.d.ts","sourceRoot":"","sources":["../../src/router/requestInterceptor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AACvC,OAAO,EAAkB,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtE,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,eAAe,EACpB,IAAI,CAAC,EAAE,GAAG,EACV,eAAe,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACtF,kBAAkB,CAuCpB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAOpD;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GACrD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAqBxB"}
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ /**
3
+ * Request Interceptor - Intercepts and analyzes MCP requests
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.interceptRequest = interceptRequest;
7
+ exports.requiresSapConfig = requiresSapConfig;
8
+ exports.sanitizeHeadersForLogging = sanitizeHeadersForLogging;
9
+ const headerAnalyzer_js_1 = require("./headerAnalyzer.js");
10
+ const logger_js_1 = require("../lib/logger.js");
11
+ /**
12
+ * Intercept and analyze incoming HTTP request
13
+ */
14
+ function interceptRequest(req, body, configOverrides) {
15
+ // Extract headers
16
+ const headers = {};
17
+ for (const [key, value] of Object.entries(req.headers)) {
18
+ headers[key] = value;
19
+ }
20
+ // Analyze headers for routing decision (with config overrides)
21
+ const routingDecision = (0, headerAnalyzer_js_1.analyzeHeaders)(req.headers, configOverrides);
22
+ // Extract session ID if present
23
+ const sessionId = (req.headers["x-session-id"] ||
24
+ req.headers["mcp-session-id"] ||
25
+ req.headers["x-mcp-session-id"]);
26
+ // Generate client ID
27
+ const clientId = `${req.socket.remoteAddress}:${req.socket.remotePort}`;
28
+ // Log intercepted request
29
+ logger_js_1.logger.debug("Request intercepted", {
30
+ type: "REQUEST_INTERCEPTED",
31
+ method: req.method,
32
+ url: req.url,
33
+ clientId,
34
+ sessionId,
35
+ routingStrategy: routingDecision.strategy,
36
+ routingReason: routingDecision.reason,
37
+ });
38
+ return {
39
+ method: req.method || "GET",
40
+ url: req.url || "/",
41
+ headers,
42
+ body,
43
+ routingDecision,
44
+ sessionId,
45
+ clientId,
46
+ };
47
+ }
48
+ /**
49
+ * Check if request requires SAP configuration
50
+ * Only tools/call requires SAP config - all other methods don't
51
+ */
52
+ function requiresSapConfig(body) {
53
+ if (!body || typeof body !== "object") {
54
+ return false;
55
+ }
56
+ const method = body.method;
57
+ return method === "tools/call";
58
+ }
59
+ /**
60
+ * Sanitize headers for logging (remove sensitive data)
61
+ */
62
+ function sanitizeHeadersForLogging(headers) {
63
+ const sanitized = {};
64
+ const sensitiveKeys = [
65
+ "authorization",
66
+ "x-sap-jwt-token",
67
+ "x-sap-refresh-token",
68
+ "x-sap-password",
69
+ "x-sap-uaa-client-secret",
70
+ ];
71
+ for (const [key, value] of Object.entries(headers)) {
72
+ if (sensitiveKeys.includes(key.toLowerCase())) {
73
+ sanitized[key] = "[REDACTED]";
74
+ }
75
+ else {
76
+ sanitized[key] = Array.isArray(value)
77
+ ? value.join(", ")
78
+ : (value || "");
79
+ }
80
+ }
81
+ return sanitized;
82
+ }