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