@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.
- package/CHANGELOG.md +89 -0
- package/LICENSE +22 -0
- package/README.md +234 -0
- package/bin/mcp-abap-adt-proxy.js +203 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +645 -0
- package/dist/lib/config.d.ts +33 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +196 -0
- package/dist/lib/errorHandler.d.ts +69 -0
- package/dist/lib/errorHandler.d.ts.map +1 -0
- package/dist/lib/errorHandler.js +202 -0
- package/dist/lib/logger.d.ts +17 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +43 -0
- package/dist/lib/stores.d.ts +18 -0
- package/dist/lib/stores.d.ts.map +1 -0
- package/dist/lib/stores.js +22 -0
- package/dist/lib/transportConfig.d.ts +18 -0
- package/dist/lib/transportConfig.d.ts.map +1 -0
- package/dist/lib/transportConfig.js +124 -0
- package/dist/proxy/cloudLlmHubProxy.d.ts +61 -0
- package/dist/proxy/cloudLlmHubProxy.d.ts.map +1 -0
- package/dist/proxy/cloudLlmHubProxy.js +380 -0
- package/dist/router/directCloudRouter.d.ts +32 -0
- package/dist/router/directCloudRouter.d.ts.map +1 -0
- package/dist/router/directCloudRouter.js +179 -0
- package/dist/router/headerAnalyzer.d.ts +48 -0
- package/dist/router/headerAnalyzer.d.ts.map +1 -0
- package/dist/router/headerAnalyzer.js +124 -0
- package/dist/router/localBasicRouter.d.ts +23 -0
- package/dist/router/localBasicRouter.d.ts.map +1 -0
- package/dist/router/localBasicRouter.js +140 -0
- package/dist/router/requestInterceptor.d.ts +32 -0
- package/dist/router/requestInterceptor.d.ts.map +1 -0
- package/dist/router/requestInterceptor.js +82 -0
- 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
|
+
}
|