@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,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration management for MCP ABAP ADT Proxy
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.loadConfig = loadConfig;
|
|
40
|
+
exports.validateConfig = validateConfig;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
/**
|
|
44
|
+
* Load configuration from file if exists, otherwise from environment variables
|
|
45
|
+
*/
|
|
46
|
+
function loadConfig(configPath) {
|
|
47
|
+
// Try to load from config file first
|
|
48
|
+
if (configPath || process.env.MCP_PROXY_CONFIG) {
|
|
49
|
+
const filePath = configPath || process.env.MCP_PROXY_CONFIG;
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(filePath)) {
|
|
52
|
+
const configContent = fs.readFileSync(filePath, "utf-8");
|
|
53
|
+
const fileConfig = JSON.parse(configContent);
|
|
54
|
+
return mergeConfig(fileConfig, loadFromEnv());
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.warn(`Failed to load config from file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Try default config file locations
|
|
62
|
+
const defaultPaths = [
|
|
63
|
+
path.join(process.cwd(), "mcp-proxy-config.json"),
|
|
64
|
+
path.join(process.cwd(), ".mcp-proxy-config.json"),
|
|
65
|
+
path.join(process.env.HOME || process.env.USERPROFILE || "", ".mcp-proxy-config.json"),
|
|
66
|
+
];
|
|
67
|
+
for (const configPath of defaultPaths) {
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(configPath)) {
|
|
70
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
71
|
+
const fileConfig = JSON.parse(configContent);
|
|
72
|
+
return mergeConfig(fileConfig, loadFromEnv());
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Continue to next path
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Fall back to environment variables only
|
|
80
|
+
return loadFromEnv();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Load configuration from environment variables and command line
|
|
84
|
+
*/
|
|
85
|
+
function loadFromEnv() {
|
|
86
|
+
// Parse command line arguments for --btp, --mcp, --mcp-url, and --unsafe
|
|
87
|
+
const btpDestination = getArgValue("--btp");
|
|
88
|
+
const mcpDestination = getArgValue("--mcp");
|
|
89
|
+
const mcpUrl = getArgValue("--mcp-url");
|
|
90
|
+
const unsafe = hasArg("--unsafe") || process.env.MCP_PROXY_UNSAFE === "true";
|
|
91
|
+
return {
|
|
92
|
+
cloudLlmHubUrl: process.env.CLOUD_LLM_HUB_URL || "",
|
|
93
|
+
httpPort: parseInt(process.env.MCP_HTTP_PORT || "3001", 10),
|
|
94
|
+
ssePort: parseInt(process.env.MCP_SSE_PORT || "3002", 10),
|
|
95
|
+
httpHost: process.env.MCP_HTTP_HOST || "0.0.0.0",
|
|
96
|
+
sseHost: process.env.MCP_SSE_HOST || "0.0.0.0",
|
|
97
|
+
logLevel: process.env.LOG_LEVEL || "info",
|
|
98
|
+
btpDestination,
|
|
99
|
+
mcpDestination,
|
|
100
|
+
mcpUrl: mcpUrl || process.env.MCP_URL,
|
|
101
|
+
unsafe,
|
|
102
|
+
maxRetries: parseInt(process.env.MCP_PROXY_MAX_RETRIES || "3", 10),
|
|
103
|
+
retryDelay: parseInt(process.env.MCP_PROXY_RETRY_DELAY || "1000", 10),
|
|
104
|
+
requestTimeout: parseInt(process.env.MCP_PROXY_REQUEST_TIMEOUT || "60000", 10),
|
|
105
|
+
circuitBreakerThreshold: parseInt(process.env.MCP_PROXY_CIRCUIT_BREAKER_THRESHOLD || "5", 10),
|
|
106
|
+
circuitBreakerTimeout: parseInt(process.env.MCP_PROXY_CIRCUIT_BREAKER_TIMEOUT || "60000", 10),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get argument value from command line
|
|
111
|
+
*/
|
|
112
|
+
function getArgValue(argName) {
|
|
113
|
+
const args = process.argv;
|
|
114
|
+
for (let i = 0; i < args.length; i++) {
|
|
115
|
+
if (args[i] === argName && i + 1 < args.length) {
|
|
116
|
+
return args[i + 1];
|
|
117
|
+
}
|
|
118
|
+
if (args[i].startsWith(`${argName}=`)) {
|
|
119
|
+
return args[i].split("=")[1];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check if argument exists in command line
|
|
126
|
+
*/
|
|
127
|
+
function hasArg(argName) {
|
|
128
|
+
const args = process.argv;
|
|
129
|
+
return args.some(arg => arg === argName || arg.startsWith(`${argName}=`));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Merge file config with environment config (env takes precedence)
|
|
133
|
+
*/
|
|
134
|
+
function mergeConfig(fileConfig, envConfig) {
|
|
135
|
+
return {
|
|
136
|
+
cloudLlmHubUrl: envConfig.cloudLlmHubUrl || fileConfig.cloudLlmHubUrl || "",
|
|
137
|
+
httpPort: envConfig.httpPort || fileConfig.httpPort || 3001,
|
|
138
|
+
ssePort: envConfig.ssePort || fileConfig.ssePort || 3002,
|
|
139
|
+
httpHost: envConfig.httpHost || fileConfig.httpHost || "0.0.0.0",
|
|
140
|
+
sseHost: envConfig.sseHost || fileConfig.sseHost || "0.0.0.0",
|
|
141
|
+
logLevel: envConfig.logLevel || fileConfig.logLevel || "info",
|
|
142
|
+
// Command line overrides take precedence
|
|
143
|
+
btpDestination: envConfig.btpDestination ?? fileConfig.btpDestination,
|
|
144
|
+
mcpDestination: envConfig.mcpDestination ?? fileConfig.mcpDestination,
|
|
145
|
+
mcpUrl: envConfig.mcpUrl ?? fileConfig.mcpUrl,
|
|
146
|
+
unsafe: envConfig.unsafe ?? fileConfig.unsafe ?? false,
|
|
147
|
+
maxRetries: envConfig.maxRetries ?? fileConfig.maxRetries ?? 3,
|
|
148
|
+
retryDelay: envConfig.retryDelay ?? fileConfig.retryDelay ?? 1000,
|
|
149
|
+
requestTimeout: envConfig.requestTimeout ?? fileConfig.requestTimeout ?? 60000,
|
|
150
|
+
circuitBreakerThreshold: envConfig.circuitBreakerThreshold ?? fileConfig.circuitBreakerThreshold ?? 5,
|
|
151
|
+
circuitBreakerTimeout: envConfig.circuitBreakerTimeout ?? fileConfig.circuitBreakerTimeout ?? 60000,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Validate configuration
|
|
156
|
+
*/
|
|
157
|
+
function validateConfig(config) {
|
|
158
|
+
const errors = [];
|
|
159
|
+
const warnings = [];
|
|
160
|
+
// Cloud LLM Hub URL is only required if we're using proxy functionality
|
|
161
|
+
// For direct cloud and basic auth, it's not needed
|
|
162
|
+
if (!config.cloudLlmHubUrl) {
|
|
163
|
+
warnings.push("CLOUD_LLM_HUB_URL is not set - proxy to cloud-llm-hub will not work");
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
try {
|
|
167
|
+
new URL(config.cloudLlmHubUrl);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
errors.push("CLOUD_LLM_HUB_URL must be a valid URL");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (config.httpPort < 1 || config.httpPort > 65535) {
|
|
174
|
+
errors.push("MCP_HTTP_PORT must be between 1 and 65535");
|
|
175
|
+
}
|
|
176
|
+
if (config.ssePort < 1 || config.ssePort > 65535) {
|
|
177
|
+
errors.push("MCP_SSE_PORT must be between 1 and 65535");
|
|
178
|
+
}
|
|
179
|
+
if (config.httpPort === config.ssePort) {
|
|
180
|
+
errors.push("MCP_HTTP_PORT and MCP_SSE_PORT must be different");
|
|
181
|
+
}
|
|
182
|
+
if (config.maxRetries && (config.maxRetries < 0 || config.maxRetries > 10)) {
|
|
183
|
+
warnings.push("MCP_PROXY_MAX_RETRIES should be between 0 and 10");
|
|
184
|
+
}
|
|
185
|
+
if (config.retryDelay && (config.retryDelay < 0 || config.retryDelay > 60000)) {
|
|
186
|
+
warnings.push("MCP_PROXY_RETRY_DELAY should be between 0 and 60000ms");
|
|
187
|
+
}
|
|
188
|
+
if (config.requestTimeout && (config.requestTimeout < 1000 || config.requestTimeout > 300000)) {
|
|
189
|
+
warnings.push("MCP_PROXY_REQUEST_TIMEOUT should be between 1000 and 300000ms");
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
valid: errors.length === 0,
|
|
193
|
+
errors,
|
|
194
|
+
warnings,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handling & Resilience utilities
|
|
3
|
+
*/
|
|
4
|
+
export interface RetryOptions {
|
|
5
|
+
maxRetries: number;
|
|
6
|
+
retryDelay: number;
|
|
7
|
+
retryableStatusCodes?: number[];
|
|
8
|
+
retryableErrors?: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if error is retryable
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRetryableError(error: unknown, retryableStatusCodes?: number[]): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Sleep for specified milliseconds
|
|
16
|
+
*/
|
|
17
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Retry function with exponential backoff
|
|
20
|
+
*/
|
|
21
|
+
export declare function retryWithBackoff<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>;
|
|
22
|
+
/**
|
|
23
|
+
* Circuit Breaker implementation
|
|
24
|
+
*/
|
|
25
|
+
export declare class CircuitBreaker {
|
|
26
|
+
private threshold;
|
|
27
|
+
private timeout;
|
|
28
|
+
private failures;
|
|
29
|
+
private lastFailureTime;
|
|
30
|
+
private state;
|
|
31
|
+
constructor(threshold?: number, timeout?: number);
|
|
32
|
+
/**
|
|
33
|
+
* Check if circuit breaker allows request
|
|
34
|
+
*/
|
|
35
|
+
canProceed(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Record success
|
|
38
|
+
*/
|
|
39
|
+
recordSuccess(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Record failure
|
|
42
|
+
*/
|
|
43
|
+
recordFailure(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get current state
|
|
46
|
+
*/
|
|
47
|
+
getState(): "closed" | "open" | "half-open";
|
|
48
|
+
/**
|
|
49
|
+
* Reset circuit breaker
|
|
50
|
+
*/
|
|
51
|
+
reset(): void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Handle token expiration errors
|
|
55
|
+
*/
|
|
56
|
+
export declare function isTokenExpirationError(error: unknown): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Create error response in MCP format
|
|
59
|
+
*/
|
|
60
|
+
export declare function createErrorResponse(id: string | number | null, code: number, message: string, data?: any): {
|
|
61
|
+
jsonrpc: string;
|
|
62
|
+
id: string | number | null;
|
|
63
|
+
error: {
|
|
64
|
+
code: number;
|
|
65
|
+
message: string;
|
|
66
|
+
data?: any;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=errorHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/lib/errorHandler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,oBAAoB,GAAE,MAAM,EAAyB,GAAG,OAAO,CAsB/G;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,CAAC,CAAC,CAgDZ;AAED;;GAEG;AACH,qBAAa,cAAc;IAMvB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,OAAO;IANjB,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,KAAK,CAA6C;gBAGhD,SAAS,GAAE,MAAU,EACrB,OAAO,GAAE,MAAc;IAGjC;;OAEG;IACH,UAAU,IAAI,OAAO;IAkBrB;;OAEG;IACH,aAAa,IAAI,IAAI;IAYrB;;OAEG;IACH,aAAa,IAAI,IAAI;IAcrB;;OAEG;IACH,QAAQ,IAAI,QAAQ,GAAG,MAAM,GAAG,WAAW;IAI3C;;OAEG;IACH,KAAK,IAAI,IAAI;CAQd;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAiB9D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,GAAG,GACT;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,GAAG,CAAC;KACZ,CAAC;CACH,CAUA"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Error Handling & Resilience utilities
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.CircuitBreaker = void 0;
|
|
10
|
+
exports.isRetryableError = isRetryableError;
|
|
11
|
+
exports.sleep = sleep;
|
|
12
|
+
exports.retryWithBackoff = retryWithBackoff;
|
|
13
|
+
exports.isTokenExpirationError = isTokenExpirationError;
|
|
14
|
+
exports.createErrorResponse = createErrorResponse;
|
|
15
|
+
const logger_js_1 = require("./logger.js");
|
|
16
|
+
/**
|
|
17
|
+
* Check if error is retryable
|
|
18
|
+
*/
|
|
19
|
+
function isRetryableError(error, retryableStatusCodes = [500, 502, 503, 504]) {
|
|
20
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
21
|
+
const status = error.response?.status;
|
|
22
|
+
if (status && retryableStatusCodes.includes(status)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
// Network errors are retryable
|
|
26
|
+
if (!error.response && error.request) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Token expiration errors are retryable (will be handled separately)
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
const message = error.message.toLowerCase();
|
|
33
|
+
if (message.includes("token") && (message.includes("expired") || message.includes("invalid"))) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Sleep for specified milliseconds
|
|
41
|
+
*/
|
|
42
|
+
function sleep(ms) {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Retry function with exponential backoff
|
|
47
|
+
*/
|
|
48
|
+
async function retryWithBackoff(fn, options) {
|
|
49
|
+
const { maxRetries, retryDelay, retryableStatusCodes = [500, 502, 503, 504] } = options;
|
|
50
|
+
let lastError;
|
|
51
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
return await fn();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
lastError = error;
|
|
57
|
+
// Don't retry on last attempt
|
|
58
|
+
if (attempt === maxRetries) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
// Check if error is retryable
|
|
62
|
+
if (!isRetryableError(error, retryableStatusCodes)) {
|
|
63
|
+
logger_js_1.logger.debug("Error is not retryable, stopping retries", {
|
|
64
|
+
type: "RETRY_STOPPED",
|
|
65
|
+
attempt,
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
});
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
// Calculate backoff delay (exponential backoff)
|
|
71
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
72
|
+
logger_js_1.logger.warn("Retrying after error", {
|
|
73
|
+
type: "RETRY_ATTEMPT",
|
|
74
|
+
attempt: attempt + 1,
|
|
75
|
+
maxRetries,
|
|
76
|
+
delay,
|
|
77
|
+
error: error instanceof Error ? error.message : String(error),
|
|
78
|
+
});
|
|
79
|
+
await sleep(delay);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// All retries exhausted
|
|
83
|
+
logger_js_1.logger.error("All retry attempts exhausted", {
|
|
84
|
+
type: "RETRY_EXHAUSTED",
|
|
85
|
+
maxRetries,
|
|
86
|
+
error: lastError instanceof Error ? lastError.message : String(lastError),
|
|
87
|
+
});
|
|
88
|
+
throw lastError;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Circuit Breaker implementation
|
|
92
|
+
*/
|
|
93
|
+
class CircuitBreaker {
|
|
94
|
+
threshold;
|
|
95
|
+
timeout;
|
|
96
|
+
failures = 0;
|
|
97
|
+
lastFailureTime = 0;
|
|
98
|
+
state = "closed";
|
|
99
|
+
constructor(threshold = 5, timeout = 60000) {
|
|
100
|
+
this.threshold = threshold;
|
|
101
|
+
this.timeout = timeout;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if circuit breaker allows request
|
|
105
|
+
*/
|
|
106
|
+
canProceed() {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (this.state === "open") {
|
|
109
|
+
// Check if timeout has passed
|
|
110
|
+
if (now - this.lastFailureTime >= this.timeout) {
|
|
111
|
+
this.state = "half-open";
|
|
112
|
+
logger_js_1.logger.info("Circuit breaker entering half-open state", {
|
|
113
|
+
type: "CIRCUIT_BREAKER_HALF_OPEN",
|
|
114
|
+
});
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Record success
|
|
123
|
+
*/
|
|
124
|
+
recordSuccess() {
|
|
125
|
+
if (this.state === "half-open") {
|
|
126
|
+
this.state = "closed";
|
|
127
|
+
this.failures = 0;
|
|
128
|
+
logger_js_1.logger.info("Circuit breaker closed after successful request", {
|
|
129
|
+
type: "CIRCUIT_BREAKER_CLOSED",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else if (this.state === "closed") {
|
|
133
|
+
this.failures = 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Record failure
|
|
138
|
+
*/
|
|
139
|
+
recordFailure() {
|
|
140
|
+
this.failures++;
|
|
141
|
+
this.lastFailureTime = Date.now();
|
|
142
|
+
if (this.failures >= this.threshold) {
|
|
143
|
+
this.state = "open";
|
|
144
|
+
logger_js_1.logger.error("Circuit breaker opened due to failures", {
|
|
145
|
+
type: "CIRCUIT_BREAKER_OPENED",
|
|
146
|
+
failures: this.failures,
|
|
147
|
+
threshold: this.threshold,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get current state
|
|
153
|
+
*/
|
|
154
|
+
getState() {
|
|
155
|
+
return this.state;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Reset circuit breaker
|
|
159
|
+
*/
|
|
160
|
+
reset() {
|
|
161
|
+
this.failures = 0;
|
|
162
|
+
this.lastFailureTime = 0;
|
|
163
|
+
this.state = "closed";
|
|
164
|
+
logger_js_1.logger.info("Circuit breaker reset", {
|
|
165
|
+
type: "CIRCUIT_BREAKER_RESET",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.CircuitBreaker = CircuitBreaker;
|
|
170
|
+
/**
|
|
171
|
+
* Handle token expiration errors
|
|
172
|
+
*/
|
|
173
|
+
function isTokenExpirationError(error) {
|
|
174
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
175
|
+
const status = error.response?.status;
|
|
176
|
+
if (status === 401 || status === 403) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (error instanceof Error) {
|
|
181
|
+
const message = error.message.toLowerCase();
|
|
182
|
+
return (message.includes("token") &&
|
|
183
|
+
(message.includes("expired") || message.includes("invalid") || message.includes("unauthorized")));
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Create error response in MCP format
|
|
189
|
+
*/
|
|
190
|
+
function createErrorResponse(id, code, message, data) {
|
|
191
|
+
return {
|
|
192
|
+
jsonrpc: "2.0",
|
|
193
|
+
id,
|
|
194
|
+
error: {
|
|
195
|
+
code,
|
|
196
|
+
message,
|
|
197
|
+
data,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Import axios for type checking
|
|
202
|
+
const axios_1 = __importDefault(require("axios"));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility for MCP ABAP ADT Proxy
|
|
3
|
+
*/
|
|
4
|
+
export declare enum LogLevel {
|
|
5
|
+
DEBUG = 0,
|
|
6
|
+
INFO = 1,
|
|
7
|
+
WARN = 2,
|
|
8
|
+
ERROR = 3
|
|
9
|
+
}
|
|
10
|
+
export interface Logger {
|
|
11
|
+
debug(message: string, meta?: Record<string, unknown>): void;
|
|
12
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
13
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
14
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
15
|
+
}
|
|
16
|
+
export declare const logger: Logger;
|
|
17
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,oBAAY,QAAQ;IAClB,KAAK,IAAI;IACT,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;CACV;AAED,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9D;AAqCD,eAAO,MAAM,MAAM,EAAE,MAIpB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Logger utility for MCP ABAP ADT Proxy
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logger = exports.LogLevel = void 0;
|
|
7
|
+
var LogLevel;
|
|
8
|
+
(function (LogLevel) {
|
|
9
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
10
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
11
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
12
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
13
|
+
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
14
|
+
class SimpleLogger {
|
|
15
|
+
level;
|
|
16
|
+
constructor(level = LogLevel.INFO) {
|
|
17
|
+
this.level = level;
|
|
18
|
+
}
|
|
19
|
+
log(level, prefix, message, meta) {
|
|
20
|
+
if (level < this.level) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const timestamp = new Date().toISOString();
|
|
24
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : '';
|
|
25
|
+
console.error(`[${timestamp}] [${prefix}] ${message}${metaStr}`);
|
|
26
|
+
}
|
|
27
|
+
debug(message, meta) {
|
|
28
|
+
this.log(LogLevel.DEBUG, 'DEBUG', message, meta);
|
|
29
|
+
}
|
|
30
|
+
info(message, meta) {
|
|
31
|
+
this.log(LogLevel.INFO, 'INFO', message, meta);
|
|
32
|
+
}
|
|
33
|
+
warn(message, meta) {
|
|
34
|
+
this.log(LogLevel.WARN, 'WARN', message, meta);
|
|
35
|
+
}
|
|
36
|
+
error(message, meta) {
|
|
37
|
+
this.log(LogLevel.ERROR, 'ERROR', message, meta);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Default logger instance
|
|
41
|
+
exports.logger = new SimpleLogger(process.env.LOG_LEVEL?.toUpperCase()
|
|
42
|
+
? (LogLevel[process.env.LOG_LEVEL.toUpperCase()] ?? LogLevel.INFO)
|
|
43
|
+
: LogLevel.INFO);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-specific stores for proxy
|
|
3
|
+
* Uses FileServiceKeyStore and either FileSessionStore or SafeSessionStore
|
|
4
|
+
* depending on the unsafe configuration parameter
|
|
5
|
+
*/
|
|
6
|
+
import { IServiceKeyStore, ISessionStore } from "@mcp-abap-adt/auth-broker";
|
|
7
|
+
/**
|
|
8
|
+
* Get platform-specific stores
|
|
9
|
+
* Returns stores based on configuration:
|
|
10
|
+
* - If unsafe=true: uses FileSessionStore (persists to disk)
|
|
11
|
+
* - If unsafe=false: uses SafeSessionStore (in-memory, secure)
|
|
12
|
+
* @param unsafe If true, use FileSessionStore. If false, use SafeSessionStore (default).
|
|
13
|
+
*/
|
|
14
|
+
export declare function getPlatformStores(unsafe?: boolean): Promise<{
|
|
15
|
+
serviceKeyStore: IServiceKeyStore;
|
|
16
|
+
sessionStore: ISessionStore;
|
|
17
|
+
}>;
|
|
18
|
+
//# sourceMappingURL=stores.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stores.d.ts","sourceRoot":"","sources":["../../src/lib/stores.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAIL,gBAAgB,EAChB,aAAa,EACd,MAAM,2BAA2B,CAAC;AAEnC;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC;IACxE,eAAe,EAAE,gBAAgB,CAAC;IAClC,YAAY,EAAE,aAAa,CAAC;CAC7B,CAAC,CAKD"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-specific stores for proxy
|
|
4
|
+
* Uses FileServiceKeyStore and either FileSessionStore or SafeSessionStore
|
|
5
|
+
* depending on the unsafe configuration parameter
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.getPlatformStores = getPlatformStores;
|
|
9
|
+
const auth_broker_1 = require("@mcp-abap-adt/auth-broker");
|
|
10
|
+
/**
|
|
11
|
+
* Get platform-specific stores
|
|
12
|
+
* Returns stores based on configuration:
|
|
13
|
+
* - If unsafe=true: uses FileSessionStore (persists to disk)
|
|
14
|
+
* - If unsafe=false: uses SafeSessionStore (in-memory, secure)
|
|
15
|
+
* @param unsafe If true, use FileSessionStore. If false, use SafeSessionStore (default).
|
|
16
|
+
*/
|
|
17
|
+
async function getPlatformStores(unsafe = false) {
|
|
18
|
+
return {
|
|
19
|
+
serviceKeyStore: new auth_broker_1.FileServiceKeyStore(),
|
|
20
|
+
sessionStore: unsafe ? new auth_broker_1.FileSessionStore() : new auth_broker_1.SafeSessionStore(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport Configuration - Parse and manage transport settings
|
|
3
|
+
*/
|
|
4
|
+
export type TransportType = "stdio" | "streamable-http" | "sse";
|
|
5
|
+
export interface TransportConfig {
|
|
6
|
+
type: TransportType;
|
|
7
|
+
host?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
enableJsonResponse?: boolean;
|
|
10
|
+
allowedOrigins?: string[];
|
|
11
|
+
allowedHosts?: string[];
|
|
12
|
+
enableDnsRebindingProtection?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse transport configuration from command line arguments and environment variables
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseTransportConfig(): TransportConfig;
|
|
18
|
+
//# sourceMappingURL=transportConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transportConfig.d.ts","sourceRoot":"","sources":["../../src/lib/transportConfig.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,iBAAiB,GAAG,KAAK,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,4BAA4B,CAAC,EAAE,OAAO,CAAC;CACxC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,eAAe,CAgGtD"}
|