@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,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transport Configuration - Parse and manage transport settings
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseTransportConfig = parseTransportConfig;
|
|
7
|
+
/**
|
|
8
|
+
* Parse transport configuration from command line arguments and environment variables
|
|
9
|
+
*/
|
|
10
|
+
function parseTransportConfig() {
|
|
11
|
+
const args = process.argv;
|
|
12
|
+
// Check for explicit transport type
|
|
13
|
+
let transportType = null;
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg.startsWith("--transport=")) {
|
|
17
|
+
const value = arg.split("=")[1].toLowerCase();
|
|
18
|
+
if (value === "stdio" || value === "streamable-http" || value === "http" || value === "sse") {
|
|
19
|
+
transportType = value === "http" ? "streamable-http" : value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (arg === "--transport" && i + 1 < args.length) {
|
|
23
|
+
const value = args[i + 1].toLowerCase();
|
|
24
|
+
if (value === "stdio" || value === "streamable-http" || value === "http" || value === "sse") {
|
|
25
|
+
transportType = value === "http" ? "streamable-http" : value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (arg === "--http")
|
|
29
|
+
transportType = "streamable-http";
|
|
30
|
+
if (arg === "--stdio")
|
|
31
|
+
transportType = "stdio";
|
|
32
|
+
if (arg === "--sse")
|
|
33
|
+
transportType = "sse";
|
|
34
|
+
}
|
|
35
|
+
// Check environment variable
|
|
36
|
+
if (!transportType && process.env.MCP_TRANSPORT) {
|
|
37
|
+
const envTransport = process.env.MCP_TRANSPORT.toLowerCase();
|
|
38
|
+
if (envTransport === "stdio" || envTransport === "streamable-http" || envTransport === "http" || envTransport === "sse") {
|
|
39
|
+
transportType = envTransport === "http" ? "streamable-http" : envTransport;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Auto-detect stdio mode: if stdin is not a TTY, we're likely in stdio mode
|
|
43
|
+
if (!transportType && !process.stdin.isTTY) {
|
|
44
|
+
transportType = "stdio";
|
|
45
|
+
}
|
|
46
|
+
// Default to streamable-http if not specified
|
|
47
|
+
if (!transportType) {
|
|
48
|
+
transportType = "streamable-http";
|
|
49
|
+
}
|
|
50
|
+
// Parse SSE config
|
|
51
|
+
if (transportType === "sse") {
|
|
52
|
+
const port = parseInt(getArgValue("--sse-port") || process.env.MCP_SSE_PORT || "3002", 10);
|
|
53
|
+
const host = getArgValue("--sse-host") || process.env.MCP_SSE_HOST || "0.0.0.0";
|
|
54
|
+
return {
|
|
55
|
+
type: "sse",
|
|
56
|
+
host,
|
|
57
|
+
port,
|
|
58
|
+
allowedOrigins: parseListOption("--sse-allowed-origins", "MCP_SSE_ALLOWED_ORIGINS"),
|
|
59
|
+
allowedHosts: parseListOption("--sse-allowed-hosts", "MCP_SSE_ALLOWED_HOSTS"),
|
|
60
|
+
enableDnsRebindingProtection: parseBooleanOption("--sse-enable-dns-protection", "MCP_SSE_ENABLE_DNS_PROTECTION", false),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Parse HTTP/streamable-http config
|
|
64
|
+
if (transportType === "streamable-http") {
|
|
65
|
+
const port = parseInt(getArgValue("--http-port") || process.env.MCP_HTTP_PORT || "3001", 10);
|
|
66
|
+
const host = getArgValue("--http-host") || process.env.MCP_HTTP_HOST || "0.0.0.0";
|
|
67
|
+
return {
|
|
68
|
+
type: "streamable-http",
|
|
69
|
+
host,
|
|
70
|
+
port,
|
|
71
|
+
enableJsonResponse: parseBooleanOption("--http-json-response", "MCP_HTTP_ENABLE_JSON_RESPONSE", false),
|
|
72
|
+
allowedOrigins: parseListOption("--http-allowed-origins", "MCP_HTTP_ALLOWED_ORIGINS"),
|
|
73
|
+
allowedHosts: parseListOption("--http-allowed-hosts", "MCP_HTTP_ALLOWED_HOSTS"),
|
|
74
|
+
enableDnsRebindingProtection: parseBooleanOption("--http-enable-dns-protection", "MCP_HTTP_ENABLE_DNS_PROTECTION", false),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Stdio config (no additional options)
|
|
78
|
+
return {
|
|
79
|
+
type: "stdio",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get argument value from command line
|
|
84
|
+
*/
|
|
85
|
+
function getArgValue(argName) {
|
|
86
|
+
const args = process.argv;
|
|
87
|
+
for (let i = 0; i < args.length; i++) {
|
|
88
|
+
if (args[i] === argName && i + 1 < args.length) {
|
|
89
|
+
return args[i + 1];
|
|
90
|
+
}
|
|
91
|
+
if (args[i].startsWith(`${argName}=`)) {
|
|
92
|
+
return args[i].split("=")[1];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse boolean option from command line or environment
|
|
99
|
+
*/
|
|
100
|
+
function parseBooleanOption(argName, envName, defaultValue) {
|
|
101
|
+
const argValue = getArgValue(argName);
|
|
102
|
+
if (argValue !== undefined) {
|
|
103
|
+
return argValue.toLowerCase() === "true" || argValue === "1";
|
|
104
|
+
}
|
|
105
|
+
const envValue = process.env[envName];
|
|
106
|
+
if (envValue !== undefined) {
|
|
107
|
+
return envValue.toLowerCase() === "true" || envValue === "1";
|
|
108
|
+
}
|
|
109
|
+
return defaultValue;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse list option from command line or environment
|
|
113
|
+
*/
|
|
114
|
+
function parseListOption(argName, envName) {
|
|
115
|
+
const argValue = getArgValue(argName);
|
|
116
|
+
if (argValue) {
|
|
117
|
+
return argValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
118
|
+
}
|
|
119
|
+
const envValue = process.env[envName];
|
|
120
|
+
if (envValue) {
|
|
121
|
+
return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud LLM Hub Proxy - Proxies requests to cloud-llm-hub with JWT authentication
|
|
3
|
+
*
|
|
4
|
+
* For requests with x-sap-destination: "sk", proxies to cloud-llm-hub
|
|
5
|
+
* with JWT token from auth-broker
|
|
6
|
+
*/
|
|
7
|
+
import { AuthBroker } from "@mcp-abap-adt/auth-broker";
|
|
8
|
+
import { RoutingDecision } from "../router/headerAnalyzer.js";
|
|
9
|
+
import { ProxyConfig } from "../lib/config.js";
|
|
10
|
+
export interface ProxyRequest {
|
|
11
|
+
method: string;
|
|
12
|
+
params?: any;
|
|
13
|
+
id?: string | number | null;
|
|
14
|
+
jsonrpc?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ProxyResponse {
|
|
17
|
+
jsonrpc: string;
|
|
18
|
+
id?: string | number | null;
|
|
19
|
+
result?: any;
|
|
20
|
+
error?: {
|
|
21
|
+
code: number;
|
|
22
|
+
message: string;
|
|
23
|
+
data?: any;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Cloud LLM Hub Proxy Client
|
|
28
|
+
*/
|
|
29
|
+
export declare class CloudLlmHubProxy {
|
|
30
|
+
private axiosInstance;
|
|
31
|
+
private authBroker;
|
|
32
|
+
private defaultBaseUrl;
|
|
33
|
+
private tokenCache;
|
|
34
|
+
private readonly TOKEN_CACHE_TTL;
|
|
35
|
+
private circuitBreaker;
|
|
36
|
+
private config;
|
|
37
|
+
constructor(defaultBaseUrl: string, authBroker: AuthBroker, config?: Partial<ProxyConfig>);
|
|
38
|
+
/**
|
|
39
|
+
* Get JWT token for destination from auth-broker with retry and token refresh
|
|
40
|
+
*/
|
|
41
|
+
private getJwtToken;
|
|
42
|
+
/**
|
|
43
|
+
* Build proxy request with JWT tokens and SAP configuration
|
|
44
|
+
*
|
|
45
|
+
* Uses service keys:
|
|
46
|
+
* 1. x-btp-destination - for BTP Cloud authorization (Authorization: Bearer token) - OPTIONAL
|
|
47
|
+
* 2. x-mcp-destination - for SAP ABAP connection (SAP headers with token and config) - OPTIONAL
|
|
48
|
+
*
|
|
49
|
+
* Can work with only x-mcp-destination (without BTP) for local testing without authentication
|
|
50
|
+
*/
|
|
51
|
+
private buildProxyRequest;
|
|
52
|
+
/**
|
|
53
|
+
* Proxy MCP request to cloud-llm-hub with retry, circuit breaker, and error handling
|
|
54
|
+
*/
|
|
55
|
+
proxyRequest(originalRequest: ProxyRequest, routingDecision: RoutingDecision, originalHeaders: Record<string, string | string[] | undefined>): Promise<ProxyResponse>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Create Cloud LLM Hub Proxy instance
|
|
59
|
+
*/
|
|
60
|
+
export declare function createCloudLlmHubProxy(cloudLlmHubUrl: string, config?: Partial<ProxyConfig>): Promise<CloudLlmHubProxy>;
|
|
61
|
+
//# sourceMappingURL=cloudLlmHubProxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudLlmHubProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/cloudLlmHubProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAc,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAU3D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,GAAG,CAAC;KACZ,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,UAAU,CAAgE;IAClF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAc;gBAEhB,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAmEzF;;OAEG;YACW,WAAW;IAwDzB;;;;;;;;OAQG;YACW,iBAAiB;IA4J/B;;OAEG;IACG,YAAY,CAChB,eAAe,EAAE,YAAY,EAC7B,eAAe,EAAE,eAAe,EAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAC7D,OAAO,CAAC,aAAa,CAAC;CA2G1B;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,cAAc,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CAY3B"}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cloud LLM Hub Proxy - Proxies requests to cloud-llm-hub with JWT authentication
|
|
4
|
+
*
|
|
5
|
+
* For requests with x-sap-destination: "sk", proxies to cloud-llm-hub
|
|
6
|
+
* with JWT token from auth-broker
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.CloudLlmHubProxy = void 0;
|
|
13
|
+
exports.createCloudLlmHubProxy = createCloudLlmHubProxy;
|
|
14
|
+
const axios_1 = __importDefault(require("axios"));
|
|
15
|
+
const auth_broker_1 = require("@mcp-abap-adt/auth-broker");
|
|
16
|
+
const logger_js_1 = require("../lib/logger.js");
|
|
17
|
+
const config_js_1 = require("../lib/config.js");
|
|
18
|
+
const stores_js_1 = require("../lib/stores.js");
|
|
19
|
+
const errorHandler_js_1 = require("../lib/errorHandler.js");
|
|
20
|
+
/**
|
|
21
|
+
* Cloud LLM Hub Proxy Client
|
|
22
|
+
*/
|
|
23
|
+
class CloudLlmHubProxy {
|
|
24
|
+
axiosInstance;
|
|
25
|
+
authBroker;
|
|
26
|
+
defaultBaseUrl;
|
|
27
|
+
tokenCache = new Map();
|
|
28
|
+
TOKEN_CACHE_TTL = 30 * 60 * 1000; // 30 minutes
|
|
29
|
+
circuitBreaker;
|
|
30
|
+
config;
|
|
31
|
+
constructor(defaultBaseUrl, authBroker, config) {
|
|
32
|
+
// Default base URL (used if x-mcp-url is relative)
|
|
33
|
+
this.defaultBaseUrl = defaultBaseUrl.replace(/\/$/, ""); // Remove trailing slash
|
|
34
|
+
this.authBroker = authBroker;
|
|
35
|
+
this.config = (0, config_js_1.loadConfig)();
|
|
36
|
+
// Merge provided config
|
|
37
|
+
if (config) {
|
|
38
|
+
this.config = { ...this.config, ...config };
|
|
39
|
+
}
|
|
40
|
+
// Initialize circuit breaker
|
|
41
|
+
this.circuitBreaker = new errorHandler_js_1.CircuitBreaker(this.config.circuitBreakerThreshold || 5, this.config.circuitBreakerTimeout || 60000);
|
|
42
|
+
// Create axios instance without baseURL - we'll use full URLs from x-mcp-url
|
|
43
|
+
this.axiosInstance = axios_1.default.create({
|
|
44
|
+
timeout: this.config.requestTimeout || 60000,
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// Add request interceptor for logging
|
|
50
|
+
this.axiosInstance.interceptors.request.use((config) => {
|
|
51
|
+
logger_js_1.logger.debug("Proxying request to cloud-llm-hub", {
|
|
52
|
+
type: "CLOUD_LLM_HUB_PROXY_REQUEST",
|
|
53
|
+
method: config.method,
|
|
54
|
+
url: config.url,
|
|
55
|
+
baseURL: config.baseURL,
|
|
56
|
+
});
|
|
57
|
+
return config;
|
|
58
|
+
}, (error) => {
|
|
59
|
+
logger_js_1.logger.error("Request interceptor error", {
|
|
60
|
+
type: "CLOUD_LLM_HUB_PROXY_REQUEST_ERROR",
|
|
61
|
+
error: error instanceof Error ? error.message : String(error),
|
|
62
|
+
});
|
|
63
|
+
return Promise.reject(error);
|
|
64
|
+
});
|
|
65
|
+
// Add response interceptor for logging
|
|
66
|
+
this.axiosInstance.interceptors.response.use((response) => {
|
|
67
|
+
logger_js_1.logger.debug("Received response from cloud-llm-hub", {
|
|
68
|
+
type: "CLOUD_LLM_HUB_PROXY_RESPONSE",
|
|
69
|
+
status: response.status,
|
|
70
|
+
url: response.config.url,
|
|
71
|
+
});
|
|
72
|
+
return response;
|
|
73
|
+
}, (error) => {
|
|
74
|
+
logger_js_1.logger.error("Response interceptor error", {
|
|
75
|
+
type: "CLOUD_LLM_HUB_PROXY_RESPONSE_ERROR",
|
|
76
|
+
error: error instanceof Error ? error.message : String(error),
|
|
77
|
+
status: error.response?.status,
|
|
78
|
+
url: error.config?.url,
|
|
79
|
+
});
|
|
80
|
+
return Promise.reject(error);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get JWT token for destination from auth-broker with retry and token refresh
|
|
85
|
+
*/
|
|
86
|
+
async getJwtToken(destination, forceRefresh = false) {
|
|
87
|
+
// Check cache first (unless force refresh)
|
|
88
|
+
if (!forceRefresh) {
|
|
89
|
+
const cached = this.tokenCache.get(destination);
|
|
90
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
91
|
+
logger_js_1.logger.debug("Using cached JWT token", {
|
|
92
|
+
type: "JWT_TOKEN_CACHE_HIT",
|
|
93
|
+
destination,
|
|
94
|
+
});
|
|
95
|
+
return cached.token;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Retry logic for token retrieval
|
|
99
|
+
const retryOptions = {
|
|
100
|
+
maxRetries: this.config.maxRetries || 3,
|
|
101
|
+
retryDelay: this.config.retryDelay || 1000,
|
|
102
|
+
retryableStatusCodes: [500, 502, 503, 504],
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
const token = await (0, errorHandler_js_1.retryWithBackoff)(async () => {
|
|
106
|
+
// Clear cache if force refresh
|
|
107
|
+
if (forceRefresh) {
|
|
108
|
+
this.tokenCache.delete(destination);
|
|
109
|
+
}
|
|
110
|
+
// Get token from auth-broker
|
|
111
|
+
const token = await this.authBroker.getToken(destination);
|
|
112
|
+
// Cache token (assume it's valid for 30 minutes)
|
|
113
|
+
this.tokenCache.set(destination, {
|
|
114
|
+
token,
|
|
115
|
+
expiresAt: Date.now() + this.TOKEN_CACHE_TTL,
|
|
116
|
+
});
|
|
117
|
+
logger_js_1.logger.debug("Retrieved JWT token from auth-broker", {
|
|
118
|
+
type: "JWT_TOKEN_RETRIEVED",
|
|
119
|
+
destination,
|
|
120
|
+
forceRefresh,
|
|
121
|
+
});
|
|
122
|
+
return token;
|
|
123
|
+
}, retryOptions);
|
|
124
|
+
return token;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
logger_js_1.logger.error("Failed to get JWT token from auth-broker after retries", {
|
|
128
|
+
type: "JWT_TOKEN_ERROR",
|
|
129
|
+
destination,
|
|
130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
131
|
+
});
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build proxy request with JWT tokens and SAP configuration
|
|
137
|
+
*
|
|
138
|
+
* Uses service keys:
|
|
139
|
+
* 1. x-btp-destination - for BTP Cloud authorization (Authorization: Bearer token) - OPTIONAL
|
|
140
|
+
* 2. x-mcp-destination - for SAP ABAP connection (SAP headers with token and config) - OPTIONAL
|
|
141
|
+
*
|
|
142
|
+
* Can work with only x-mcp-destination (without BTP) for local testing without authentication
|
|
143
|
+
*/
|
|
144
|
+
async buildProxyRequest(originalRequest, routingDecision, originalHeaders, forceTokenRefresh = false) {
|
|
145
|
+
// Build headers for target MCP server
|
|
146
|
+
const proxyHeaders = {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
};
|
|
149
|
+
// 1. Get authorization token for BTP Cloud (x-btp-destination or --btp) - OPTIONAL
|
|
150
|
+
// This token is used for Authorization: Bearer header to connect to cloud MCP server
|
|
151
|
+
// Only required if btpDestination is present
|
|
152
|
+
if (routingDecision.btpDestination) {
|
|
153
|
+
const authToken = await this.getJwtToken(routingDecision.btpDestination, forceTokenRefresh);
|
|
154
|
+
proxyHeaders["Authorization"] = `Bearer ${authToken}`;
|
|
155
|
+
logger_js_1.logger.debug("Added BTP Cloud authorization token", {
|
|
156
|
+
type: "BTP_AUTH_TOKEN_ADDED",
|
|
157
|
+
destination: routingDecision.btpDestination,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
logger_js_1.logger.debug("Skipping BTP Cloud authorization (no btpDestination - local testing mode)", {
|
|
162
|
+
type: "BTP_AUTH_SKIPPED",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// 2. Get SAP ABAP configuration from x-mcp-destination (OPTIONAL)
|
|
166
|
+
// This provides token and configuration for SAP ABAP connection
|
|
167
|
+
// Only used if x-mcp-destination or --mcp is provided
|
|
168
|
+
// For local testing (without BTP), token retrieval is optional - URL is still needed
|
|
169
|
+
if (routingDecision.mcpDestination) {
|
|
170
|
+
const sapUrl = await this.authBroker.getSapUrl(routingDecision.mcpDestination);
|
|
171
|
+
// Try to get token, but don't fail if it's not available (for local testing)
|
|
172
|
+
try {
|
|
173
|
+
const sapToken = await this.getJwtToken(routingDecision.mcpDestination, forceTokenRefresh);
|
|
174
|
+
proxyHeaders["x-sap-jwt-token"] = sapToken;
|
|
175
|
+
logger_js_1.logger.debug("Added SAP ABAP token", {
|
|
176
|
+
type: "SAP_TOKEN_ADDED",
|
|
177
|
+
destination: routingDecision.mcpDestination,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
logger_js_1.logger.warn("Failed to get SAP ABAP token (continuing without token for local testing)", {
|
|
182
|
+
type: "SAP_TOKEN_SKIPPED",
|
|
183
|
+
destination: routingDecision.mcpDestination,
|
|
184
|
+
error: error instanceof Error ? error.message : String(error),
|
|
185
|
+
});
|
|
186
|
+
// Continue without token - this is OK for local testing
|
|
187
|
+
}
|
|
188
|
+
// Add SAP ABAP headers
|
|
189
|
+
if (sapUrl) {
|
|
190
|
+
proxyHeaders["x-sap-url"] = sapUrl;
|
|
191
|
+
}
|
|
192
|
+
proxyHeaders["x-sap-destination"] = routingDecision.mcpDestination;
|
|
193
|
+
logger_js_1.logger.debug("Added SAP ABAP configuration", {
|
|
194
|
+
type: "SAP_CONFIG_ADDED",
|
|
195
|
+
destination: routingDecision.mcpDestination,
|
|
196
|
+
sapUrl: sapUrl || "not found",
|
|
197
|
+
hasToken: !!proxyHeaders["x-sap-jwt-token"],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// Preserve other original SAP headers if provided
|
|
201
|
+
const sapHeaders = [
|
|
202
|
+
"x-sap-client",
|
|
203
|
+
"x-sap-auth-type",
|
|
204
|
+
"x-sap-login",
|
|
205
|
+
"x-sap-password",
|
|
206
|
+
];
|
|
207
|
+
for (const headerName of sapHeaders) {
|
|
208
|
+
const value = originalHeaders[headerName];
|
|
209
|
+
if (value) {
|
|
210
|
+
proxyHeaders[headerName] = Array.isArray(value) ? value[0] : value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Get MCP server URL from:
|
|
214
|
+
// 1. x-mcp-url header (if provided) - direct URL
|
|
215
|
+
// 2. BTP destination service key (if btpDestination is present)
|
|
216
|
+
// 3. MCP destination service key (if only mcpDestination is present)
|
|
217
|
+
let baseUrl;
|
|
218
|
+
if (routingDecision.mcpUrl) {
|
|
219
|
+
// Use direct URL from x-mcp-url header
|
|
220
|
+
baseUrl = routingDecision.mcpUrl;
|
|
221
|
+
logger_js_1.logger.debug("Using MCP URL from x-mcp-url header", {
|
|
222
|
+
type: "MCP_URL_FROM_HEADER",
|
|
223
|
+
url: baseUrl,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (routingDecision.btpDestination) {
|
|
227
|
+
// Get URL from BTP destination service key
|
|
228
|
+
baseUrl = await this.authBroker.getSapUrl(routingDecision.btpDestination);
|
|
229
|
+
if (!baseUrl) {
|
|
230
|
+
throw new Error(`Failed to get MCP server URL from BTP destination "${routingDecision.btpDestination}". Check service key file.`);
|
|
231
|
+
}
|
|
232
|
+
logger_js_1.logger.debug("Using MCP URL from BTP destination service key", {
|
|
233
|
+
type: "MCP_URL_FROM_BTP",
|
|
234
|
+
destination: routingDecision.btpDestination,
|
|
235
|
+
url: baseUrl,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
else if (routingDecision.mcpDestination) {
|
|
239
|
+
// Get URL from MCP destination service key (for local testing without BTP)
|
|
240
|
+
baseUrl = await this.authBroker.getSapUrl(routingDecision.mcpDestination);
|
|
241
|
+
if (!baseUrl) {
|
|
242
|
+
throw new Error(`Failed to get MCP server URL from MCP destination "${routingDecision.mcpDestination}". Check service key file.`);
|
|
243
|
+
}
|
|
244
|
+
logger_js_1.logger.debug("Using MCP URL from MCP destination service key (local testing mode)", {
|
|
245
|
+
type: "MCP_URL_FROM_MCP",
|
|
246
|
+
destination: routingDecision.mcpDestination,
|
|
247
|
+
url: baseUrl,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
throw new Error("Cannot determine MCP server URL: neither x-mcp-url header, btpDestination, nor mcpDestination is provided");
|
|
252
|
+
}
|
|
253
|
+
// Construct full MCP endpoint URL
|
|
254
|
+
// If baseUrl already contains the path, use it as-is
|
|
255
|
+
// Otherwise, append default endpoint /mcp/stream/http
|
|
256
|
+
let fullUrl;
|
|
257
|
+
if (baseUrl.includes("/mcp/") || baseUrl.endsWith("/mcp")) {
|
|
258
|
+
// URL already contains MCP path
|
|
259
|
+
fullUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// Append default endpoint
|
|
263
|
+
const mcpPath = "/mcp/stream/http";
|
|
264
|
+
fullUrl = baseUrl.endsWith("/")
|
|
265
|
+
? `${baseUrl.slice(0, -1)}${mcpPath}`
|
|
266
|
+
: `${baseUrl}${mcpPath}`;
|
|
267
|
+
}
|
|
268
|
+
logger_js_1.logger.debug("Built proxy request", {
|
|
269
|
+
type: "PROXY_REQUEST_BUILT",
|
|
270
|
+
btpDestination: routingDecision.btpDestination,
|
|
271
|
+
mcpDestination: routingDecision.mcpDestination,
|
|
272
|
+
mcpUrl: routingDecision.mcpUrl,
|
|
273
|
+
baseUrl,
|
|
274
|
+
fullUrl,
|
|
275
|
+
hasAuthToken: !!proxyHeaders["Authorization"],
|
|
276
|
+
hasSapConfig: !!proxyHeaders["x-sap-jwt-token"],
|
|
277
|
+
sapHeaders: Object.keys(proxyHeaders).filter(h => h.startsWith("x-sap")),
|
|
278
|
+
});
|
|
279
|
+
// Return axios config with full URL
|
|
280
|
+
return {
|
|
281
|
+
method: "POST",
|
|
282
|
+
url: fullUrl,
|
|
283
|
+
headers: proxyHeaders,
|
|
284
|
+
data: originalRequest,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Proxy MCP request to cloud-llm-hub with retry, circuit breaker, and error handling
|
|
289
|
+
*/
|
|
290
|
+
async proxyRequest(originalRequest, routingDecision, originalHeaders) {
|
|
291
|
+
// Check circuit breaker
|
|
292
|
+
if (!this.circuitBreaker.canProceed()) {
|
|
293
|
+
logger_js_1.logger.warn("Circuit breaker is open, rejecting request", {
|
|
294
|
+
type: "CIRCUIT_BREAKER_REJECTED",
|
|
295
|
+
btpDestination: routingDecision.btpDestination,
|
|
296
|
+
mcpDestination: routingDecision.mcpDestination,
|
|
297
|
+
});
|
|
298
|
+
return (0, errorHandler_js_1.createErrorResponse)(originalRequest.id || null, -32001, "Service temporarily unavailable (circuit breaker open)", { circuitBreakerState: this.circuitBreaker.getState() });
|
|
299
|
+
}
|
|
300
|
+
const retryOptions = {
|
|
301
|
+
maxRetries: this.config.maxRetries || 3,
|
|
302
|
+
retryDelay: this.config.retryDelay || 1000,
|
|
303
|
+
retryableStatusCodes: [500, 502, 503, 504],
|
|
304
|
+
};
|
|
305
|
+
try {
|
|
306
|
+
const response = await (0, errorHandler_js_1.retryWithBackoff)(async () => {
|
|
307
|
+
// Check for token expiration and refresh if needed
|
|
308
|
+
let forceTokenRefresh = false;
|
|
309
|
+
// Build proxy request (will get fresh token if needed)
|
|
310
|
+
const proxyConfig = await this.buildProxyRequest(originalRequest, routingDecision, originalHeaders, forceTokenRefresh);
|
|
311
|
+
// Send request to cloud-llm-hub
|
|
312
|
+
const response = await this.axiosInstance.request(proxyConfig);
|
|
313
|
+
// Record success in circuit breaker
|
|
314
|
+
this.circuitBreaker.recordSuccess();
|
|
315
|
+
logger_js_1.logger.debug("Proxied request completed", {
|
|
316
|
+
type: "PROXY_REQUEST_COMPLETED",
|
|
317
|
+
status: response.status,
|
|
318
|
+
hasResult: !!response.data?.result,
|
|
319
|
+
hasError: !!response.data?.error,
|
|
320
|
+
});
|
|
321
|
+
return response;
|
|
322
|
+
}, retryOptions);
|
|
323
|
+
return response.data;
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
// Record failure in circuit breaker
|
|
327
|
+
this.circuitBreaker.recordFailure();
|
|
328
|
+
// Handle token expiration
|
|
329
|
+
if ((0, errorHandler_js_1.isTokenExpirationError)(error)) {
|
|
330
|
+
logger_js_1.logger.warn("Token expiration detected, will retry with fresh token", {
|
|
331
|
+
type: "TOKEN_EXPIRATION_DETECTED",
|
|
332
|
+
btpDestination: routingDecision.btpDestination,
|
|
333
|
+
mcpDestination: routingDecision.mcpDestination,
|
|
334
|
+
});
|
|
335
|
+
// Retry once with fresh token
|
|
336
|
+
try {
|
|
337
|
+
const proxyConfig = await this.buildProxyRequest(originalRequest, routingDecision, originalHeaders, true // Force token refresh
|
|
338
|
+
);
|
|
339
|
+
const response = await this.axiosInstance.request(proxyConfig);
|
|
340
|
+
this.circuitBreaker.recordSuccess();
|
|
341
|
+
return response.data;
|
|
342
|
+
}
|
|
343
|
+
catch (retryError) {
|
|
344
|
+
logger_js_1.logger.error("Failed to retry with fresh token", {
|
|
345
|
+
type: "TOKEN_REFRESH_RETRY_FAILED",
|
|
346
|
+
error: retryError instanceof Error ? retryError.message : String(retryError),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
logger_js_1.logger.error("Failed to proxy request to cloud-llm-hub", {
|
|
351
|
+
type: "PROXY_REQUEST_ERROR",
|
|
352
|
+
error: error instanceof Error ? error.message : String(error),
|
|
353
|
+
status: axios_1.default.isAxiosError(error) ? error.response?.status : undefined,
|
|
354
|
+
circuitBreakerState: this.circuitBreaker.getState(),
|
|
355
|
+
});
|
|
356
|
+
// Return error response in MCP format
|
|
357
|
+
return (0, errorHandler_js_1.createErrorResponse)(originalRequest.id || null, axios_1.default.isAxiosError(error) && error.response?.status
|
|
358
|
+
? error.response.status
|
|
359
|
+
: -32000, error instanceof Error ? error.message : "Unknown error", {
|
|
360
|
+
circuitBreakerState: this.circuitBreaker.getState(),
|
|
361
|
+
originalError: axios_1.default.isAxiosError(error) && error.response?.data
|
|
362
|
+
? error.response.data
|
|
363
|
+
: undefined,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
exports.CloudLlmHubProxy = CloudLlmHubProxy;
|
|
369
|
+
/**
|
|
370
|
+
* Create Cloud LLM Hub Proxy instance
|
|
371
|
+
*/
|
|
372
|
+
async function createCloudLlmHubProxy(cloudLlmHubUrl, config) {
|
|
373
|
+
const unsafe = config?.unsafe ?? false;
|
|
374
|
+
const { serviceKeyStore, sessionStore } = await (0, stores_js_1.getPlatformStores)(unsafe);
|
|
375
|
+
const authBroker = new auth_broker_1.AuthBroker({
|
|
376
|
+
serviceKeyStore,
|
|
377
|
+
sessionStore,
|
|
378
|
+
}, "system");
|
|
379
|
+
return new CloudLlmHubProxy(cloudLlmHubUrl, authBroker, config);
|
|
380
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct Cloud Router - Routes requests directly to cloud ABAP systems
|
|
3
|
+
*
|
|
4
|
+
* For requests with x-sap-destination (not "sk"), routes directly to cloud ABAP
|
|
5
|
+
* using the same approach as mcp-abap-adt
|
|
6
|
+
*/
|
|
7
|
+
import { IncomingHttpHeaders } from "http";
|
|
8
|
+
import { AuthBroker } from "@mcp-abap-adt/auth-broker";
|
|
9
|
+
import { AbapConnection } from "@mcp-abap-adt/connection";
|
|
10
|
+
import { RoutingDecision } from "./headerAnalyzer.js";
|
|
11
|
+
export interface DirectCloudConfig {
|
|
12
|
+
sapUrl: string;
|
|
13
|
+
destination?: string;
|
|
14
|
+
authType: "jwt" | "basic";
|
|
15
|
+
jwtToken?: string;
|
|
16
|
+
refreshToken?: string;
|
|
17
|
+
username?: string;
|
|
18
|
+
password?: string;
|
|
19
|
+
sapClient?: string;
|
|
20
|
+
uaaUrl?: string;
|
|
21
|
+
uaaClientId?: string;
|
|
22
|
+
uaaClientSecret?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Convert validated auth config to direct cloud config
|
|
26
|
+
*/
|
|
27
|
+
export declare function createDirectCloudConfig(routingDecision: RoutingDecision, headers: IncomingHttpHeaders): DirectCloudConfig | null;
|
|
28
|
+
/**
|
|
29
|
+
* Get or create ABAP connection for direct cloud routing
|
|
30
|
+
*/
|
|
31
|
+
export declare function getDirectCloudConnection(sessionId: string, config: DirectCloudConfig, authBroker?: AuthBroker): Promise<AbapConnection>;
|
|
32
|
+
//# sourceMappingURL=directCloudRouter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directCloudRouter.d.ts","sourceRoot":"","sources":["../../src/router/directCloudRouter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAwB,cAAc,EAAsB,MAAM,0BAA0B,CAAC;AAEpG,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,KAAK,GAAG,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,mBAAmB,GAC3B,iBAAiB,GAAG,IAAI,CAgD1B;AA4CD;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,iBAAiB,EACzB,UAAU,CAAC,EAAE,UAAU,GACtB,OAAO,CAAC,cAAc,CAAC,CA+FzB"}
|