@mcp-abap-adt/proxy 0.1.5 → 0.1.8
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 +66 -1
- package/README.md +73 -65
- package/bin/mcp-abap-adt-proxy.js +15 -42
- package/dist/index.d.ts +3 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +414 -277
- package/dist/lib/config.d.ts +13 -4
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +110 -93
- package/dist/lib/errorHandler.d.ts +3 -3
- package/dist/lib/errorHandler.d.ts.map +1 -1
- package/dist/lib/errorHandler.js +31 -28
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +12 -9
- package/dist/lib/stores.d.ts +7 -7
- package/dist/lib/stores.d.ts.map +1 -1
- package/dist/lib/stores.js +46 -80
- package/dist/lib/transportConfig.d.ts +1 -1
- package/dist/lib/transportConfig.d.ts.map +1 -1
- package/dist/lib/transportConfig.js +58 -38
- package/dist/proxy/btpProxy.d.ts +102 -0
- package/dist/proxy/btpProxy.d.ts.map +1 -0
- package/dist/proxy/btpProxy.js +833 -0
- package/dist/proxy/cloudLlmHubProxy.d.ts +1 -65
- package/dist/proxy/cloudLlmHubProxy.d.ts.map +1 -1
- package/dist/proxy/cloudLlmHubProxy.js +1 -441
- package/dist/router/headerAnalyzer.d.ts +9 -18
- package/dist/router/headerAnalyzer.d.ts.map +1 -1
- package/dist/router/headerAnalyzer.js +35 -63
- package/dist/router/requestInterceptor.d.ts +5 -11
- package/dist/router/requestInterceptor.d.ts.map +1 -1
- package/dist/router/requestInterceptor.js +33 -30
- package/package.json +33 -15
- package/dist/router/directCloudRouter.d.ts +0 -32
- package/dist/router/directCloudRouter.d.ts.map +0 -1
- package/dist/router/directCloudRouter.js +0 -146
- package/dist/router/localBasicRouter.d.ts +0 -23
- package/dist/router/localBasicRouter.d.ts.map +0 -1
- package/dist/router/localBasicRouter.js +0 -118
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
"use strict";
|
|
3
2
|
/**
|
|
4
3
|
* MCP ABAP ADT Proxy Server
|
|
@@ -6,24 +5,31 @@
|
|
|
6
5
|
* Proxies local MCP requests to MCP servers with optional JWT authentication.
|
|
7
6
|
* Routes requests based on authentication headers:
|
|
8
7
|
* - x-btp-destination (or --btp): BTP Cloud authorization with JWT token
|
|
9
|
-
* - x-mcp-destination (or --mcp): SAP ABAP connection configuration
|
|
10
8
|
*
|
|
11
|
-
* Supports
|
|
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)
|
|
9
|
+
* Supports BTP authentication mode: Requires x-btp-destination or --btp parameter
|
|
14
10
|
*/
|
|
15
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
12
|
exports.McpAbapAdtProxyServer = void 0;
|
|
13
|
+
const node_http_1 = require("node:http");
|
|
14
|
+
const interfaces_1 = require("@mcp-abap-adt/interfaces");
|
|
17
15
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
18
|
-
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
19
16
|
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
20
|
-
const
|
|
21
|
-
const transportConfig_js_1 = require("./lib/transportConfig.js");
|
|
17
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
22
18
|
const config_js_1 = require("./lib/config.js");
|
|
23
19
|
const logger_js_1 = require("./lib/logger.js");
|
|
24
|
-
const
|
|
20
|
+
const transportConfig_js_1 = require("./lib/transportConfig.js");
|
|
21
|
+
const btpProxy_js_1 = require("./proxy/btpProxy.js");
|
|
25
22
|
const headerAnalyzer_js_1 = require("./router/headerAnalyzer.js");
|
|
26
|
-
const
|
|
23
|
+
const requestInterceptor_js_1 = require("./router/requestInterceptor.js");
|
|
24
|
+
/**
|
|
25
|
+
* Safely extract id from unknown body object
|
|
26
|
+
*/
|
|
27
|
+
function getBodyId(body) {
|
|
28
|
+
if (body && typeof body === 'object' && body !== null && 'id' in body) {
|
|
29
|
+
return body.id;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
27
33
|
/**
|
|
28
34
|
* MCP ABAP ADT Proxy Server
|
|
29
35
|
*/
|
|
@@ -32,30 +38,31 @@ class McpAbapAdtProxyServer {
|
|
|
32
38
|
transportConfig;
|
|
33
39
|
config;
|
|
34
40
|
httpServer;
|
|
35
|
-
|
|
41
|
+
btpProxy;
|
|
36
42
|
constructor(transportConfig, configPath) {
|
|
37
43
|
this.transportConfig = transportConfig || (0, transportConfig_js_1.parseTransportConfig)();
|
|
38
44
|
this.config = (0, config_js_1.loadConfig)(configPath);
|
|
39
45
|
// Validate configuration
|
|
40
|
-
if (this.transportConfig.type ===
|
|
46
|
+
if (this.transportConfig.type === 'streamable-http' ||
|
|
47
|
+
this.transportConfig.type === 'sse') {
|
|
41
48
|
const validation = (0, config_js_1.validateConfig)(this.config);
|
|
42
49
|
if (!validation.valid) {
|
|
43
|
-
logger_js_1.logger
|
|
44
|
-
type:
|
|
50
|
+
logger_js_1.logger?.error('Configuration validation failed', {
|
|
51
|
+
type: 'CONFIG_VALIDATION_ERROR',
|
|
45
52
|
errors: validation.errors,
|
|
46
53
|
});
|
|
47
|
-
throw new Error(`Configuration validation failed: ${validation.errors.join(
|
|
54
|
+
throw new Error(`Configuration validation failed: ${validation.errors.join(', ')}`);
|
|
48
55
|
}
|
|
49
56
|
if (validation.warnings.length > 0) {
|
|
50
|
-
logger_js_1.logger
|
|
51
|
-
type:
|
|
57
|
+
logger_js_1.logger?.warn('Configuration validation warnings', {
|
|
58
|
+
type: 'CONFIG_VALIDATION_WARNINGS',
|
|
52
59
|
warnings: validation.warnings,
|
|
53
60
|
});
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
this.server = new mcp_js_1.McpServer({
|
|
57
|
-
name:
|
|
58
|
-
version:
|
|
64
|
+
name: '@mcp-abap-adt/proxy',
|
|
65
|
+
version: '0.1.0',
|
|
59
66
|
}, {
|
|
60
67
|
capabilities: {
|
|
61
68
|
tools: {},
|
|
@@ -71,92 +78,21 @@ class McpAbapAdtProxyServer {
|
|
|
71
78
|
// The server automatically handles initialize and tools/list requests
|
|
72
79
|
// For stdio, we'll intercept requests through server.server message handler
|
|
73
80
|
}
|
|
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
81
|
/**
|
|
146
82
|
* Run the proxy server
|
|
147
83
|
*/
|
|
148
84
|
async run() {
|
|
149
|
-
if (this.transportConfig.type ===
|
|
150
|
-
// Check if
|
|
151
|
-
if (!this.config.btpDestination
|
|
152
|
-
logger_js_1.logger
|
|
153
|
-
type:
|
|
85
|
+
if (this.transportConfig.type === 'stdio') {
|
|
86
|
+
// Check if --btp parameter is provided (required for stdio)
|
|
87
|
+
if (!this.config.btpDestination) {
|
|
88
|
+
logger_js_1.logger?.error('--btp parameter is required for stdio transport', {
|
|
89
|
+
type: 'STDIO_DESTINATION_REQUIRED',
|
|
154
90
|
});
|
|
155
|
-
throw new Error(
|
|
91
|
+
throw new Error('--btp parameter is required for stdio transport. Use --btp=<destination> for BTP destination.');
|
|
156
92
|
}
|
|
157
93
|
// For stdio, we need to register a proxy tool that forwards all requests
|
|
158
94
|
// Since we can't intercept all requests directly, we'll register a tool
|
|
159
|
-
// that the client can call, which will proxy to
|
|
95
|
+
// that the client can call, which will proxy to BtpProxy
|
|
160
96
|
// Note: This requires the client to know about the proxy tool
|
|
161
97
|
// Alternatively, we could use a custom transport wrapper
|
|
162
98
|
// Simple stdio setup - MCP SDK will handle JSON-RPC protocol
|
|
@@ -166,23 +102,38 @@ class McpAbapAdtProxyServer {
|
|
|
166
102
|
// For now, we'll just connect and let the SDK handle it
|
|
167
103
|
// The actual proxying will need to be done through registered tools
|
|
168
104
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
105
|
+
const msg = `MCP Proxy Server started (Transport: stdio)
|
|
106
|
+
Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
107
|
+
console.error(msg);
|
|
108
|
+
logger_js_1.logger?.info('MCP Proxy Server started (stdio transport)', {
|
|
109
|
+
type: 'SERVER_STARTED',
|
|
110
|
+
transport: 'stdio',
|
|
173
111
|
btpDestination: this.config.btpDestination,
|
|
174
|
-
|
|
175
|
-
|
|
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)",
|
|
112
|
+
note: 'For stdio transport, --btp parameter is used as default destination',
|
|
113
|
+
mode: 'BTP authentication',
|
|
178
114
|
});
|
|
179
115
|
return;
|
|
180
116
|
}
|
|
181
|
-
|
|
117
|
+
// Eager initialization of BTP authentication
|
|
118
|
+
if (this.config.btpDestination) {
|
|
119
|
+
try {
|
|
120
|
+
if (!this.btpProxy) {
|
|
121
|
+
this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
|
|
122
|
+
}
|
|
123
|
+
// Trigger initialization (background) - this will open the browser
|
|
124
|
+
this.btpProxy.initialize(this.config.btpDestination);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
logger_js_1.logger?.error('Failed to initialize BTP proxy for eager auth', {
|
|
128
|
+
error: error instanceof Error ? error.message : String(error),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (this.transportConfig.type === 'streamable-http') {
|
|
182
133
|
await this.startHttpServer();
|
|
183
134
|
return;
|
|
184
135
|
}
|
|
185
|
-
if (this.transportConfig.type ===
|
|
136
|
+
if (this.transportConfig.type === 'sse') {
|
|
186
137
|
await this.startSseServer();
|
|
187
138
|
return;
|
|
188
139
|
}
|
|
@@ -193,25 +144,23 @@ class McpAbapAdtProxyServer {
|
|
|
193
144
|
*/
|
|
194
145
|
async startHttpServer() {
|
|
195
146
|
const httpConfig = this.transportConfig;
|
|
196
|
-
this.httpServer = (0,
|
|
197
|
-
// Log incoming HTTP request (
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
147
|
+
this.httpServer = (0, node_http_1.createServer)(async (req, res) => {
|
|
148
|
+
// Log incoming HTTP request (always log in DEBUG mode)
|
|
149
|
+
const _debugEnabled = process.env.DEBUG === 'true' ||
|
|
150
|
+
process.env.DEBUG_HTTP_REQUESTS === 'true';
|
|
151
|
+
const sanitizedHeaders = (0, requestInterceptor_js_1.sanitizeHeadersForLogging)(req.headers);
|
|
152
|
+
logger_js_1.logger?.info('=== HTTP REQUEST RECEIVED ===', {
|
|
153
|
+
type: 'HTTP_REQUEST_RECEIVED',
|
|
154
|
+
method: req.method,
|
|
155
|
+
url: req.url,
|
|
156
|
+
headers: sanitizedHeaders,
|
|
157
|
+
remoteAddress: req.socket.remoteAddress,
|
|
158
|
+
});
|
|
159
|
+
// Allow all HTTP methods (GET, POST, PUT, DELETE, etc.)
|
|
160
|
+
// if (req.method !== 'POST') {
|
|
161
|
+
// logger?.warn('Non-POST request rejected', { ... });
|
|
162
|
+
// ...
|
|
163
|
+
// }
|
|
215
164
|
// Read request body
|
|
216
165
|
let body;
|
|
217
166
|
try {
|
|
@@ -219,36 +168,93 @@ class McpAbapAdtProxyServer {
|
|
|
219
168
|
for await (const chunk of req) {
|
|
220
169
|
chunks.push(chunk);
|
|
221
170
|
}
|
|
222
|
-
const bodyString = Buffer.concat(chunks).toString(
|
|
223
|
-
|
|
171
|
+
const bodyString = Buffer.concat(chunks).toString('utf-8');
|
|
172
|
+
logger_js_1.logger?.info('=== PARSING REQUEST BODY ===', {
|
|
173
|
+
type: 'REQUEST_BODY_PARSING',
|
|
174
|
+
bodyLength: bodyString.length,
|
|
175
|
+
bodyPreview: bodyString.substring(0, 200),
|
|
176
|
+
});
|
|
177
|
+
if (bodyString.trim().length > 0) {
|
|
178
|
+
body = JSON.parse(bodyString);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
body = undefined;
|
|
182
|
+
}
|
|
183
|
+
// Sanitize body for logging
|
|
184
|
+
const sanitizedBody = {};
|
|
185
|
+
if (body && typeof body === 'object' && body !== null) {
|
|
186
|
+
const bodyObj = body;
|
|
187
|
+
sanitizedBody.method = bodyObj.method;
|
|
188
|
+
sanitizedBody.id = bodyObj.id;
|
|
189
|
+
sanitizedBody.jsonrpc = bodyObj.jsonrpc;
|
|
190
|
+
if (bodyObj.params &&
|
|
191
|
+
typeof bodyObj.params === 'object' &&
|
|
192
|
+
bodyObj.params !== null) {
|
|
193
|
+
const params = bodyObj.params;
|
|
194
|
+
sanitizedBody.params = {};
|
|
195
|
+
const sanitizedParams = sanitizedBody.params;
|
|
196
|
+
if (params.arguments &&
|
|
197
|
+
typeof params.arguments === 'object' &&
|
|
198
|
+
params.arguments !== null) {
|
|
199
|
+
sanitizedParams.arguments = {};
|
|
200
|
+
const sanitizedArgs = sanitizedParams.arguments;
|
|
201
|
+
for (const [key, value] of Object.entries(params.arguments)) {
|
|
202
|
+
const lowerKey = key.toLowerCase();
|
|
203
|
+
if (lowerKey.includes('password') ||
|
|
204
|
+
lowerKey.includes('token') ||
|
|
205
|
+
lowerKey.includes('secret')) {
|
|
206
|
+
sanitizedArgs[key] = '[REDACTED]';
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
sanitizedArgs[key] = value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const [key, value] of Object.entries(params)) {
|
|
214
|
+
if (key === 'arguments')
|
|
215
|
+
continue;
|
|
216
|
+
sanitizedParams[key] = value;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
logger_js_1.logger?.info('=== REQUEST BODY PARSED ===', {
|
|
221
|
+
type: 'REQUEST_BODY_PARSED',
|
|
222
|
+
body: sanitizedBody,
|
|
223
|
+
});
|
|
224
224
|
}
|
|
225
225
|
catch (error) {
|
|
226
|
-
logger_js_1.logger
|
|
227
|
-
type:
|
|
226
|
+
logger_js_1.logger?.error('=== FAILED TO PARSE REQUEST BODY ===', {
|
|
227
|
+
type: 'REQUEST_PARSE_ERROR',
|
|
228
228
|
error: error instanceof Error ? error.message : String(error),
|
|
229
|
+
errorStack: error instanceof Error ? error.stack : undefined,
|
|
229
230
|
});
|
|
230
|
-
res.writeHead(400, {
|
|
231
|
-
res.end(
|
|
231
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
232
|
+
res.end('Invalid request body');
|
|
232
233
|
return;
|
|
233
234
|
}
|
|
234
235
|
// Intercept and analyze request
|
|
235
|
-
// Pass config overrides (--btp, --mcp, and --mcp-url) to header analyzer
|
|
236
236
|
const configOverrides = {
|
|
237
237
|
btpDestination: this.config.btpDestination,
|
|
238
|
-
|
|
239
|
-
mcpUrl: this.config.mcpUrl,
|
|
238
|
+
targetUrl: this.config.targetUrl,
|
|
240
239
|
};
|
|
240
|
+
logger_js_1.logger?.info('=== INTERCEPTING REQUEST ===', {
|
|
241
|
+
type: 'REQUEST_INTERCEPTING',
|
|
242
|
+
configOverrides,
|
|
243
|
+
});
|
|
241
244
|
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
|
|
242
245
|
// Check routing decision
|
|
243
246
|
if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
|
|
244
|
-
logger_js_1.logger
|
|
245
|
-
type:
|
|
247
|
+
logger_js_1.logger?.error('Routing decision failed', {
|
|
248
|
+
type: 'ROUTING_DECISION_FAILED',
|
|
246
249
|
reason: intercepted.routingDecision.reason,
|
|
247
250
|
});
|
|
248
|
-
res.writeHead(400, {
|
|
251
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
252
|
+
const bodyId = body && typeof body === 'object' && body !== null && 'id' in body
|
|
253
|
+
? body.id
|
|
254
|
+
: null;
|
|
249
255
|
res.end(JSON.stringify({
|
|
250
|
-
jsonrpc:
|
|
251
|
-
id:
|
|
256
|
+
jsonrpc: '2.0',
|
|
257
|
+
id: bodyId || null,
|
|
252
258
|
error: {
|
|
253
259
|
code: -32602,
|
|
254
260
|
message: intercepted.routingDecision.reason,
|
|
@@ -257,43 +263,47 @@ class McpAbapAdtProxyServer {
|
|
|
257
263
|
return;
|
|
258
264
|
}
|
|
259
265
|
// Log proxy request
|
|
260
|
-
logger_js_1.logger
|
|
261
|
-
type:
|
|
266
|
+
logger_js_1.logger?.info('=== PROXYING REQUEST ===', {
|
|
267
|
+
type: 'PROXY_REQUEST_START',
|
|
262
268
|
btpDestination: intercepted.routingDecision.btpDestination,
|
|
263
|
-
mcpDestination: intercepted.routingDecision.mcpDestination,
|
|
264
|
-
requiresSapConfig: (0, requestInterceptor_js_1.requiresSapConfig)(body),
|
|
265
269
|
});
|
|
266
270
|
// Handle proxy request - add JWT and forward to x-mcp-url
|
|
267
271
|
try {
|
|
268
272
|
await this.handleProxyRequest(intercepted, req, res);
|
|
269
273
|
}
|
|
270
274
|
catch (error) {
|
|
271
|
-
logger_js_1.logger
|
|
272
|
-
type:
|
|
275
|
+
logger_js_1.logger?.error('Failed to process request', {
|
|
276
|
+
type: 'REQUEST_PROCESS_ERROR',
|
|
273
277
|
error: error instanceof Error ? error.message : String(error),
|
|
274
278
|
strategy: intercepted.routingDecision.strategy,
|
|
275
279
|
});
|
|
276
280
|
if (!res.headersSent) {
|
|
277
|
-
res.writeHead(500, {
|
|
278
|
-
res.end(
|
|
281
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
282
|
+
res.end('Internal server error');
|
|
279
283
|
}
|
|
280
284
|
}
|
|
281
285
|
});
|
|
282
286
|
const port = httpConfig.port || 3001;
|
|
283
|
-
const host = httpConfig.host ||
|
|
287
|
+
const host = httpConfig.host || '0.0.0.0';
|
|
284
288
|
return new Promise((resolve, reject) => {
|
|
285
|
-
this.httpServer
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
+
this.httpServer?.listen(port, host, () => {
|
|
290
|
+
const authSourceObj = this.config.btpDestination
|
|
291
|
+
? `BTP destination "${this.config.btpDestination}"`
|
|
292
|
+
: 'Waiting for "x-btp-destination" header in requests';
|
|
293
|
+
const msg = `MCP Proxy Server started on port ${port} (Transport: http)
|
|
294
|
+
Authorization source: ${authSourceObj}`;
|
|
295
|
+
console.log(msg);
|
|
296
|
+
logger_js_1.logger?.info('MCP Proxy Server started (HTTP transport)', {
|
|
297
|
+
type: 'SERVER_STARTED',
|
|
298
|
+
transport: 'streamable-http',
|
|
289
299
|
host,
|
|
290
300
|
port,
|
|
291
301
|
});
|
|
292
302
|
resolve();
|
|
293
303
|
});
|
|
294
|
-
this.httpServer
|
|
295
|
-
logger_js_1.logger
|
|
296
|
-
type:
|
|
304
|
+
this.httpServer?.on('error', (error) => {
|
|
305
|
+
logger_js_1.logger?.error('Failed to start HTTP server', {
|
|
306
|
+
type: 'SERVER_START_ERROR',
|
|
297
307
|
error: error.message,
|
|
298
308
|
});
|
|
299
309
|
reject(error);
|
|
@@ -305,57 +315,184 @@ class McpAbapAdtProxyServer {
|
|
|
305
315
|
*/
|
|
306
316
|
async handleProxyRequest(intercepted, req, res) {
|
|
307
317
|
const btpDestination = intercepted.routingDecision.btpDestination;
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
318
|
+
// Log incoming request details
|
|
319
|
+
const incomingHeaders = {};
|
|
320
|
+
for (const [key, value] of Object.entries(intercepted.headers)) {
|
|
321
|
+
const lowerKey = key.toLowerCase();
|
|
322
|
+
if (lowerKey.includes('token') ||
|
|
323
|
+
lowerKey.includes('authorization') ||
|
|
324
|
+
lowerKey.includes('password') ||
|
|
325
|
+
lowerKey.includes('secret')) {
|
|
326
|
+
incomingHeaders[key] = '[REDACTED]';
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
incomingHeaders[key] = Array.isArray(value)
|
|
330
|
+
? value.join(', ')
|
|
331
|
+
: value || '';
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const sanitizedBody = {};
|
|
335
|
+
if (intercepted.body &&
|
|
336
|
+
typeof intercepted.body === 'object' &&
|
|
337
|
+
intercepted.body !== null) {
|
|
338
|
+
const bodyObj = intercepted.body;
|
|
339
|
+
if (bodyObj.params &&
|
|
340
|
+
typeof bodyObj.params === 'object' &&
|
|
341
|
+
bodyObj.params !== null) {
|
|
342
|
+
const params = bodyObj.params;
|
|
343
|
+
sanitizedBody.params = {};
|
|
344
|
+
const sanitizedParams = sanitizedBody.params;
|
|
345
|
+
// For tools/call, log arguments
|
|
346
|
+
if (params.arguments &&
|
|
347
|
+
typeof params.arguments === 'object' &&
|
|
348
|
+
params.arguments !== null) {
|
|
349
|
+
sanitizedParams.arguments = {};
|
|
350
|
+
const sanitizedArgs = sanitizedParams.arguments;
|
|
351
|
+
for (const [key, value] of Object.entries(params.arguments)) {
|
|
352
|
+
const lowerKey = key.toLowerCase();
|
|
353
|
+
if (lowerKey.includes('password') ||
|
|
354
|
+
lowerKey.includes('token') ||
|
|
355
|
+
lowerKey.includes('secret')) {
|
|
356
|
+
sanitizedArgs[key] = '[REDACTED]';
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
sanitizedArgs[key] = value;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Log other params
|
|
364
|
+
for (const [key, value] of Object.entries(params)) {
|
|
365
|
+
if (key === 'arguments')
|
|
366
|
+
continue;
|
|
367
|
+
sanitizedParams[key] = value;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
sanitizedBody.method = bodyObj.method;
|
|
371
|
+
sanitizedBody.id = bodyObj.id;
|
|
372
|
+
sanitizedBody.jsonrpc = bodyObj.jsonrpc;
|
|
373
|
+
}
|
|
374
|
+
// Use original request body structure for proxying (generic support)
|
|
375
|
+
// We do NOT enforce JSON-RPC structure here anymore, to support REST/OData
|
|
376
|
+
const proxyRequestData = intercepted.body;
|
|
377
|
+
logger_js_1.logger?.info('=== INCOMING REQUEST ===', {
|
|
378
|
+
type: 'PROXY_REQUEST_INCOMING',
|
|
379
|
+
method: req.method,
|
|
380
|
+
url: req.url,
|
|
312
381
|
btpDestination,
|
|
313
|
-
mcpDestination,
|
|
314
|
-
mcpUrl,
|
|
315
382
|
sessionId: intercepted.sessionId,
|
|
316
|
-
|
|
383
|
+
headers: incomingHeaders,
|
|
384
|
+
body: sanitizedBody,
|
|
385
|
+
mode: 'BTP authentication',
|
|
317
386
|
});
|
|
318
387
|
try {
|
|
319
388
|
// Ensure proxy is initialized (URL will be obtained from service key for btpDestination or mcpDestination)
|
|
320
|
-
if (!this.
|
|
321
|
-
|
|
322
|
-
const baseUrl = this.config.cloudLlmHubUrl || "https://default.example.com";
|
|
323
|
-
this.cloudLlmHubProxy = await (0, cloudLlmHubProxy_js_1.createCloudLlmHubProxy)(baseUrl, this.config);
|
|
389
|
+
if (!this.btpProxy) {
|
|
390
|
+
this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
|
|
324
391
|
}
|
|
325
392
|
// Build MCP request from intercepted request
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
393
|
+
// Note: Use ?? instead of || to preserve falsy values like 0
|
|
394
|
+
const interceptedBodyId = getBodyId(intercepted.body);
|
|
395
|
+
const interceptedBodyIdType = typeof interceptedBodyId;
|
|
396
|
+
const interceptedBodyIdUndefined = interceptedBodyId === undefined;
|
|
397
|
+
logger_js_1.logger?.info('=== BEFORE BUILDING MCP REQUEST ===', {
|
|
398
|
+
type: 'BEFORE_BUILD_MCP_REQUEST',
|
|
399
|
+
interceptedBodyId,
|
|
400
|
+
interceptedBodyIdType,
|
|
401
|
+
interceptedBodyIdUndefined,
|
|
402
|
+
interceptedBodyIdNull: interceptedBodyId === null,
|
|
403
|
+
interceptedBodyIdZero: interceptedBodyId === 0,
|
|
404
|
+
interceptedBodyIdFalsy: !interceptedBodyId,
|
|
405
|
+
fullInterceptedBody: JSON.stringify(intercepted.body),
|
|
406
|
+
});
|
|
407
|
+
// Preserve id correctly - 0 is a valid id value
|
|
408
|
+
let mcpRequestId;
|
|
409
|
+
if (interceptedBodyId === undefined) {
|
|
410
|
+
mcpRequestId = null;
|
|
411
|
+
}
|
|
412
|
+
else if (typeof interceptedBodyId === 'string' ||
|
|
413
|
+
typeof interceptedBodyId === 'number') {
|
|
414
|
+
mcpRequestId = interceptedBodyId;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
mcpRequestId = null;
|
|
418
|
+
}
|
|
419
|
+
// Proxy request to BtpProxy
|
|
420
|
+
// Pass the original request data directly
|
|
421
|
+
const proxyRequest = {
|
|
422
|
+
method: intercepted.method, // Use original HTTP method (GET, POST, etc.)
|
|
423
|
+
url: intercepted.url, // Pass request URL (path)
|
|
424
|
+
data: proxyRequestData, // Use original body (or undefined)
|
|
425
|
+
id: mcpRequestId, // Keep ID for logging context if available
|
|
426
|
+
};
|
|
427
|
+
const proxyResponse = await this.btpProxy.proxyRequest(proxyRequest, intercepted.routingDecision, intercepted.headers);
|
|
428
|
+
// Fix: If backend returned null ID but we entered with a valid ID (e.g. 0), restore it.
|
|
429
|
+
// Cline/Zod schemas reject null ID for successful responses if request had an ID.
|
|
430
|
+
if ((proxyResponse.id === null || proxyResponse.id === undefined) &&
|
|
431
|
+
mcpRequestId !== null &&
|
|
432
|
+
mcpRequestId !== undefined) {
|
|
433
|
+
logger_js_1.logger?.debug('Restoring original request ID in response', {
|
|
434
|
+
originalId: mcpRequestId,
|
|
435
|
+
receivedId: proxyResponse.id,
|
|
436
|
+
});
|
|
437
|
+
proxyResponse.id = mcpRequestId;
|
|
438
|
+
}
|
|
439
|
+
logger_js_1.logger?.info('=== FORWARDING REQUEST ===', {
|
|
440
|
+
type: 'PROXY_REQUEST_FORWARDING',
|
|
441
|
+
interceptedBodyIdAtForward: getBodyId(intercepted.body),
|
|
442
|
+
proxyRequest,
|
|
443
|
+
routingDecision: intercepted.routingDecision,
|
|
444
|
+
headers: intercepted.headers,
|
|
445
|
+
});
|
|
446
|
+
// Log response
|
|
447
|
+
const sanitizedResponse = {
|
|
448
|
+
jsonrpc: proxyResponse.jsonrpc,
|
|
449
|
+
id: proxyResponse.id,
|
|
331
450
|
};
|
|
332
|
-
|
|
333
|
-
|
|
451
|
+
if (proxyResponse.result) {
|
|
452
|
+
sanitizedResponse.result = proxyResponse.result;
|
|
453
|
+
}
|
|
454
|
+
if (proxyResponse.error) {
|
|
455
|
+
sanitizedResponse.error = {
|
|
456
|
+
code: proxyResponse.error.code,
|
|
457
|
+
message: proxyResponse.error.message,
|
|
458
|
+
data: proxyResponse.error.data,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
logger_js_1.logger?.info('=== RESPONSE FROM TARGET SERVICE ===', {
|
|
462
|
+
type: 'PROXY_RESPONSE_RECEIVED',
|
|
463
|
+
response: sanitizedResponse,
|
|
464
|
+
hasResult: !!proxyResponse.result,
|
|
465
|
+
hasError: !!proxyResponse.error,
|
|
466
|
+
});
|
|
334
467
|
// Send response back to client
|
|
335
|
-
res.writeHead(200, {
|
|
468
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
336
469
|
res.end(JSON.stringify(proxyResponse));
|
|
337
|
-
logger_js_1.logger
|
|
338
|
-
type:
|
|
470
|
+
logger_js_1.logger?.info('=== RESPONSE SENT TO CLIENT ===', {
|
|
471
|
+
type: 'PROXY_RESPONSE_SENT',
|
|
472
|
+
statusCode: 200,
|
|
473
|
+
});
|
|
474
|
+
logger_js_1.logger?.debug('BtpProxy request completed', {
|
|
475
|
+
type: 'BTP_PROXY_COMPLETED',
|
|
339
476
|
hasResult: !!proxyResponse.result,
|
|
340
477
|
hasError: !!proxyResponse.error,
|
|
341
478
|
});
|
|
342
479
|
}
|
|
343
480
|
catch (error) {
|
|
344
481
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
345
|
-
logger_js_1.logger
|
|
346
|
-
type:
|
|
482
|
+
logger_js_1.logger?.error('Failed to handle BtpProxy request', {
|
|
483
|
+
type: 'BTP_PROXY_ERROR',
|
|
347
484
|
error: errorMessage,
|
|
348
485
|
stack: error instanceof Error ? error.stack : undefined,
|
|
349
486
|
});
|
|
350
487
|
// Output error to stderr for user visibility (only if verbose mode is enabled)
|
|
351
|
-
if ((0,
|
|
488
|
+
if ((0, btpProxy_js_1.shouldWriteStderr)()) {
|
|
352
489
|
process.stderr.write(`[MCP Proxy] ✗ Connection error: ${errorMessage}\n`);
|
|
353
490
|
}
|
|
354
491
|
if (!res.headersSent) {
|
|
355
|
-
res.writeHead(500, {
|
|
492
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
356
493
|
res.end(JSON.stringify({
|
|
357
|
-
jsonrpc:
|
|
358
|
-
id: intercepted.body
|
|
494
|
+
jsonrpc: '2.0',
|
|
495
|
+
id: getBodyId(intercepted.body) || null,
|
|
359
496
|
error: {
|
|
360
497
|
code: -32000,
|
|
361
498
|
message: errorMessage,
|
|
@@ -370,36 +507,38 @@ class McpAbapAdtProxyServer {
|
|
|
370
507
|
async startSseServer() {
|
|
371
508
|
const sseConfig = this.transportConfig;
|
|
372
509
|
const streamPathMap = new Map([
|
|
373
|
-
[
|
|
374
|
-
[
|
|
375
|
-
[
|
|
510
|
+
['/', '/messages'],
|
|
511
|
+
['/mcp/events', '/mcp/messages'],
|
|
512
|
+
['/sse', '/messages'],
|
|
376
513
|
]);
|
|
377
|
-
const
|
|
514
|
+
const _streamPaths = Array.from(streamPathMap.keys());
|
|
378
515
|
const postPathSet = new Set(streamPathMap.values());
|
|
379
|
-
postPathSet.add(
|
|
380
|
-
postPathSet.add(
|
|
381
|
-
const httpServer = (0,
|
|
516
|
+
postPathSet.add('/messages');
|
|
517
|
+
postPathSet.add('/mcp/messages');
|
|
518
|
+
const httpServer = (0, node_http_1.createServer)(async (req, res) => {
|
|
382
519
|
// SSE: Always restrict to local connections only
|
|
383
520
|
const remoteAddress = req.socket.remoteAddress;
|
|
384
|
-
const isLocal = remoteAddress ===
|
|
385
|
-
remoteAddress ===
|
|
386
|
-
remoteAddress ===
|
|
387
|
-
|
|
521
|
+
const isLocal = remoteAddress === '127.0.0.1' ||
|
|
522
|
+
remoteAddress === '::1' ||
|
|
523
|
+
remoteAddress === 'localhost' ||
|
|
524
|
+
remoteAddress?.startsWith('127.');
|
|
388
525
|
if (!isLocal) {
|
|
389
|
-
logger_js_1.logger
|
|
390
|
-
type:
|
|
526
|
+
logger_js_1.logger?.warn('SSE: Non-local connection rejected', {
|
|
527
|
+
type: 'SSE_NON_LOCAL_REJECTED',
|
|
391
528
|
remoteAddress,
|
|
392
529
|
});
|
|
393
|
-
res.writeHead(403, {
|
|
394
|
-
res.end(
|
|
530
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
531
|
+
res.end('Forbidden: SSE transport only accepts local connections');
|
|
395
532
|
return;
|
|
396
533
|
}
|
|
397
|
-
const requestUrl = req.url
|
|
398
|
-
|
|
399
|
-
|
|
534
|
+
const requestUrl = req.url
|
|
535
|
+
? new URL(req.url, `http://${req.headers.host ?? `${sseConfig.host}:${sseConfig.port}`}`)
|
|
536
|
+
: undefined;
|
|
537
|
+
let pathname = requestUrl?.pathname ?? '/';
|
|
538
|
+
if (pathname.length > 1 && pathname.endsWith('/')) {
|
|
400
539
|
pathname = pathname.slice(0, -1);
|
|
401
540
|
}
|
|
402
|
-
// Apply config overrides (--btp
|
|
541
|
+
// Apply config overrides (--btp) to headers if not present
|
|
403
542
|
const headers = {};
|
|
404
543
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
405
544
|
if (value) {
|
|
@@ -407,33 +546,27 @@ class McpAbapAdtProxyServer {
|
|
|
407
546
|
}
|
|
408
547
|
}
|
|
409
548
|
// Add config overrides if not present in headers
|
|
410
|
-
if (this.config.btpDestination && !headers[
|
|
411
|
-
headers[
|
|
412
|
-
}
|
|
413
|
-
if (this.config.mcpDestination && !headers["x-mcp-destination"]) {
|
|
414
|
-
headers["x-mcp-destination"] = this.config.mcpDestination;
|
|
415
|
-
}
|
|
416
|
-
if (this.config.mcpUrl && !headers["x-mcp-url"]) {
|
|
417
|
-
headers["x-mcp-url"] = this.config.mcpUrl;
|
|
549
|
+
if (this.config.btpDestination && !headers[interfaces_1.HEADER_BTP_DESTINATION]) {
|
|
550
|
+
headers[interfaces_1.HEADER_BTP_DESTINATION] = this.config.btpDestination;
|
|
418
551
|
}
|
|
419
|
-
logger_js_1.logger
|
|
420
|
-
type:
|
|
552
|
+
logger_js_1.logger?.debug('SSE request received', {
|
|
553
|
+
type: 'SSE_HTTP_REQUEST',
|
|
421
554
|
method: req.method,
|
|
422
555
|
pathname,
|
|
423
556
|
originalUrl: req.url,
|
|
424
557
|
});
|
|
425
558
|
// GET /sse, /mcp/events, or / - establish SSE connection
|
|
426
|
-
if (req.method ===
|
|
427
|
-
const postEndpoint = streamPathMap.get(pathname) ??
|
|
428
|
-
logger_js_1.logger
|
|
429
|
-
type:
|
|
559
|
+
if (req.method === 'GET' && streamPathMap.has(pathname)) {
|
|
560
|
+
const postEndpoint = streamPathMap.get(pathname) ?? '/messages';
|
|
561
|
+
logger_js_1.logger?.debug('SSE client connecting', {
|
|
562
|
+
type: 'SSE_CLIENT_CONNECTING',
|
|
430
563
|
pathname,
|
|
431
564
|
postEndpoint,
|
|
432
565
|
});
|
|
433
566
|
// Create new McpServer instance for this session
|
|
434
567
|
const sessionServer = new mcp_js_1.McpServer({
|
|
435
|
-
name:
|
|
436
|
-
version:
|
|
568
|
+
name: '@mcp-abap-adt/proxy',
|
|
569
|
+
version: '0.1.0',
|
|
437
570
|
}, {
|
|
438
571
|
capabilities: {
|
|
439
572
|
tools: {},
|
|
@@ -446,29 +579,29 @@ class McpAbapAdtProxyServer {
|
|
|
446
579
|
enableDnsRebindingProtection: sseConfig.enableDnsRebindingProtection,
|
|
447
580
|
});
|
|
448
581
|
const sessionId = transport.sessionId;
|
|
449
|
-
logger_js_1.logger
|
|
450
|
-
type:
|
|
582
|
+
logger_js_1.logger?.info('New SSE session created', {
|
|
583
|
+
type: 'SSE_SESSION_CREATED',
|
|
451
584
|
sessionId,
|
|
452
585
|
pathname,
|
|
453
586
|
});
|
|
454
587
|
// Connect transport to server
|
|
455
588
|
try {
|
|
456
589
|
await sessionServer.server.connect(transport);
|
|
457
|
-
logger_js_1.logger
|
|
458
|
-
type:
|
|
590
|
+
logger_js_1.logger?.info('SSE transport connected', {
|
|
591
|
+
type: 'SSE_CONNECTION_READY',
|
|
459
592
|
sessionId,
|
|
460
593
|
pathname,
|
|
461
594
|
postEndpoint,
|
|
462
595
|
});
|
|
463
596
|
}
|
|
464
597
|
catch (error) {
|
|
465
|
-
logger_js_1.logger
|
|
466
|
-
type:
|
|
598
|
+
logger_js_1.logger?.error('Failed to connect SSE transport', {
|
|
599
|
+
type: 'SSE_CONNECT_ERROR',
|
|
467
600
|
error: error instanceof Error ? error.message : String(error),
|
|
468
601
|
sessionId,
|
|
469
602
|
});
|
|
470
603
|
if (!res.headersSent) {
|
|
471
|
-
res.writeHead(500).end(
|
|
604
|
+
res.writeHead(500).end('Internal Server Error');
|
|
472
605
|
}
|
|
473
606
|
else {
|
|
474
607
|
res.end();
|
|
@@ -476,17 +609,17 @@ class McpAbapAdtProxyServer {
|
|
|
476
609
|
return;
|
|
477
610
|
}
|
|
478
611
|
// Cleanup on connection close
|
|
479
|
-
res.on(
|
|
480
|
-
logger_js_1.logger
|
|
481
|
-
type:
|
|
612
|
+
res.on('close', () => {
|
|
613
|
+
logger_js_1.logger?.info('SSE connection closed', {
|
|
614
|
+
type: 'SSE_CONNECTION_CLOSED',
|
|
482
615
|
sessionId,
|
|
483
616
|
pathname,
|
|
484
617
|
});
|
|
485
618
|
sessionServer.server.close();
|
|
486
619
|
});
|
|
487
620
|
transport.onerror = (error) => {
|
|
488
|
-
logger_js_1.logger
|
|
489
|
-
type:
|
|
621
|
+
logger_js_1.logger?.error('SSE transport error', {
|
|
622
|
+
type: 'SSE_TRANSPORT_ERROR',
|
|
490
623
|
error: error instanceof Error ? error.message : String(error),
|
|
491
624
|
sessionId,
|
|
492
625
|
});
|
|
@@ -494,30 +627,30 @@ class McpAbapAdtProxyServer {
|
|
|
494
627
|
return;
|
|
495
628
|
}
|
|
496
629
|
// POST /messages or /mcp/messages - handle client messages
|
|
497
|
-
if (req.method ===
|
|
630
|
+
if (req.method === 'POST' && postPathSet.has(pathname)) {
|
|
498
631
|
// Extract sessionId from query string or header
|
|
499
632
|
let sessionId;
|
|
500
633
|
if (requestUrl) {
|
|
501
|
-
sessionId = requestUrl.searchParams.get(
|
|
634
|
+
sessionId = requestUrl.searchParams.get('sessionId') || undefined;
|
|
502
635
|
}
|
|
503
636
|
if (!sessionId) {
|
|
504
|
-
sessionId = req.headers[
|
|
637
|
+
sessionId = req.headers['x-session-id'];
|
|
505
638
|
}
|
|
506
|
-
logger_js_1.logger
|
|
507
|
-
type:
|
|
639
|
+
logger_js_1.logger?.debug('SSE POST request received', {
|
|
640
|
+
type: 'SSE_POST_REQUEST',
|
|
508
641
|
sessionId,
|
|
509
642
|
pathname,
|
|
510
643
|
});
|
|
511
644
|
if (!sessionId) {
|
|
512
|
-
logger_js_1.logger
|
|
513
|
-
type:
|
|
645
|
+
logger_js_1.logger?.error('Missing sessionId in SSE POST request', {
|
|
646
|
+
type: 'SSE_MISSING_SESSION_ID',
|
|
514
647
|
pathname,
|
|
515
648
|
});
|
|
516
|
-
res.writeHead(400, {
|
|
517
|
-
jsonrpc:
|
|
649
|
+
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({
|
|
650
|
+
jsonrpc: '2.0',
|
|
518
651
|
error: {
|
|
519
652
|
code: -32000,
|
|
520
|
-
message:
|
|
653
|
+
message: 'Missing sessionId',
|
|
521
654
|
},
|
|
522
655
|
id: null,
|
|
523
656
|
}));
|
|
@@ -531,20 +664,20 @@ class McpAbapAdtProxyServer {
|
|
|
531
664
|
chunks.push(chunk);
|
|
532
665
|
}
|
|
533
666
|
if (chunks.length > 0) {
|
|
534
|
-
const bodyString = Buffer.concat(chunks).toString(
|
|
667
|
+
const bodyString = Buffer.concat(chunks).toString('utf-8');
|
|
535
668
|
body = JSON.parse(bodyString);
|
|
536
669
|
}
|
|
537
670
|
}
|
|
538
671
|
catch (error) {
|
|
539
|
-
logger_js_1.logger
|
|
540
|
-
type:
|
|
672
|
+
logger_js_1.logger?.error('Failed to parse SSE POST request body', {
|
|
673
|
+
type: 'SSE_POST_PARSE_ERROR',
|
|
541
674
|
error: error instanceof Error ? error.message : String(error),
|
|
542
675
|
});
|
|
543
|
-
res.writeHead(400, {
|
|
544
|
-
jsonrpc:
|
|
676
|
+
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({
|
|
677
|
+
jsonrpc: '2.0',
|
|
545
678
|
error: {
|
|
546
679
|
code: -32700,
|
|
547
|
-
message:
|
|
680
|
+
message: 'Parse error',
|
|
548
681
|
},
|
|
549
682
|
id: null,
|
|
550
683
|
}));
|
|
@@ -553,19 +686,17 @@ class McpAbapAdtProxyServer {
|
|
|
553
686
|
// Intercept and analyze request with config overrides
|
|
554
687
|
const configOverrides = {
|
|
555
688
|
btpDestination: this.config.btpDestination,
|
|
556
|
-
mcpDestination: this.config.mcpDestination,
|
|
557
|
-
mcpUrl: this.config.mcpUrl,
|
|
558
689
|
};
|
|
559
690
|
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
|
|
560
691
|
// Check routing decision
|
|
561
692
|
if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
|
|
562
|
-
logger_js_1.logger
|
|
563
|
-
type:
|
|
693
|
+
logger_js_1.logger?.error('Routing decision failed for SSE POST', {
|
|
694
|
+
type: 'SSE_POST_ROUTING_FAILED',
|
|
564
695
|
reason: intercepted.routingDecision.reason,
|
|
565
696
|
});
|
|
566
|
-
res.writeHead(400, {
|
|
567
|
-
jsonrpc:
|
|
568
|
-
id: body
|
|
697
|
+
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({
|
|
698
|
+
jsonrpc: '2.0',
|
|
699
|
+
id: getBodyId(body) || null,
|
|
569
700
|
error: {
|
|
570
701
|
code: -32602,
|
|
571
702
|
message: intercepted.routingDecision.reason,
|
|
@@ -578,18 +709,18 @@ class McpAbapAdtProxyServer {
|
|
|
578
709
|
await this.handleProxyRequest(intercepted, req, res);
|
|
579
710
|
}
|
|
580
711
|
catch (error) {
|
|
581
|
-
logger_js_1.logger
|
|
582
|
-
type:
|
|
712
|
+
logger_js_1.logger?.error('Failed to process SSE POST request', {
|
|
713
|
+
type: 'SSE_POST_PROCESS_ERROR',
|
|
583
714
|
error: error instanceof Error ? error.message : String(error),
|
|
584
715
|
});
|
|
585
716
|
if (!res.headersSent) {
|
|
586
|
-
res.writeHead(500, {
|
|
717
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
587
718
|
res.end(JSON.stringify({
|
|
588
|
-
jsonrpc:
|
|
589
|
-
id: body
|
|
719
|
+
jsonrpc: '2.0',
|
|
720
|
+
id: getBodyId(body) || null,
|
|
590
721
|
error: {
|
|
591
722
|
code: -32000,
|
|
592
|
-
message: error instanceof Error ? error.message :
|
|
723
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
593
724
|
},
|
|
594
725
|
}));
|
|
595
726
|
}
|
|
@@ -597,25 +728,31 @@ class McpAbapAdtProxyServer {
|
|
|
597
728
|
return;
|
|
598
729
|
}
|
|
599
730
|
// Unknown path or method
|
|
600
|
-
res.writeHead(404, {
|
|
601
|
-
res.end(
|
|
731
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
732
|
+
res.end('Not found');
|
|
602
733
|
});
|
|
603
734
|
const port = sseConfig.port || 3002;
|
|
604
|
-
const host = sseConfig.host ||
|
|
735
|
+
const host = sseConfig.host || '0.0.0.0';
|
|
605
736
|
return new Promise((resolve, reject) => {
|
|
606
737
|
httpServer.listen(port, host, () => {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
738
|
+
const authSourceObj = this.config.btpDestination
|
|
739
|
+
? `BTP destination "${this.config.btpDestination}"`
|
|
740
|
+
: 'Waiting for "x-btp-destination" header in requests';
|
|
741
|
+
const msg = `MCP Proxy Server started on port ${port} (Transport: sse)
|
|
742
|
+
Authorization source: ${authSourceObj}`;
|
|
743
|
+
console.log(msg);
|
|
744
|
+
logger_js_1.logger?.info('MCP Proxy Server started (SSE transport)', {
|
|
745
|
+
type: 'SERVER_STARTED',
|
|
746
|
+
transport: 'sse',
|
|
610
747
|
host,
|
|
611
748
|
port,
|
|
612
749
|
});
|
|
613
750
|
this.httpServer = httpServer;
|
|
614
751
|
resolve();
|
|
615
752
|
});
|
|
616
|
-
httpServer.on(
|
|
617
|
-
logger_js_1.logger
|
|
618
|
-
type:
|
|
753
|
+
httpServer.on('error', (error) => {
|
|
754
|
+
logger_js_1.logger?.error('Failed to start SSE server', {
|
|
755
|
+
type: 'SERVER_START_ERROR',
|
|
619
756
|
error: error.message,
|
|
620
757
|
});
|
|
621
758
|
reject(error);
|
|
@@ -630,15 +767,15 @@ class McpAbapAdtProxyServer {
|
|
|
630
767
|
await this.server.close();
|
|
631
768
|
}
|
|
632
769
|
catch (error) {
|
|
633
|
-
logger_js_1.logger
|
|
634
|
-
type:
|
|
770
|
+
logger_js_1.logger?.error('Failed to close MCP server', {
|
|
771
|
+
type: 'SERVER_SHUTDOWN_ERROR',
|
|
635
772
|
error: error instanceof Error ? error.message : String(error),
|
|
636
773
|
});
|
|
637
774
|
}
|
|
638
775
|
if (this.httpServer) {
|
|
639
776
|
return new Promise((resolve) => {
|
|
640
|
-
this.httpServer
|
|
641
|
-
logger_js_1.logger
|
|
777
|
+
this.httpServer?.close(() => {
|
|
778
|
+
logger_js_1.logger?.info('HTTP server closed');
|
|
642
779
|
resolve();
|
|
643
780
|
});
|
|
644
781
|
});
|
|
@@ -649,18 +786,18 @@ exports.McpAbapAdtProxyServer = McpAbapAdtProxyServer;
|
|
|
649
786
|
// Export for use in bin script
|
|
650
787
|
exports.default = McpAbapAdtProxyServer;
|
|
651
788
|
// Auto-start server when run directly (unless MCP_SKIP_AUTO_START is set)
|
|
652
|
-
if (process.env.MCP_SKIP_AUTO_START !==
|
|
789
|
+
if (process.env.MCP_SKIP_AUTO_START !== 'true') {
|
|
653
790
|
const server = new McpAbapAdtProxyServer();
|
|
654
791
|
server.run().catch((error) => {
|
|
655
|
-
logger_js_1.logger
|
|
656
|
-
type:
|
|
792
|
+
logger_js_1.logger?.error('Fatal error while running MCP proxy server', {
|
|
793
|
+
type: 'SERVER_FATAL_ERROR',
|
|
657
794
|
error: error instanceof Error ? error.message : String(error),
|
|
658
795
|
});
|
|
659
796
|
// Always write fatal errors to stderr (even in non-verbose mode, as these are critical)
|
|
660
797
|
// But skip in test environment
|
|
661
|
-
const isTestEnv = process.env.NODE_ENV ===
|
|
798
|
+
const isTestEnv = process.env.NODE_ENV === 'test' ||
|
|
662
799
|
process.env.JEST_WORKER_ID !== undefined ||
|
|
663
|
-
typeof globalThis.jest !==
|
|
800
|
+
typeof globalThis.jest !== 'undefined';
|
|
664
801
|
if (!isTestEnv) {
|
|
665
802
|
process.stderr.write(`[MCP Proxy] ✗ Fatal error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
666
803
|
}
|