@mcp-abap-adt/proxy 0.1.4 → 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 -63
- 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 +440 -273
- package/dist/lib/config.d.ts +13 -4
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +110 -92
- 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 +15 -1
- package/dist/lib/stores.d.ts +7 -6
- package/dist/lib/stores.d.ts.map +1 -1
- package/dist/lib/stores.js +121 -8
- 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 -60
- package/dist/proxy/cloudLlmHubProxy.d.ts.map +1 -1
- package/dist/proxy/cloudLlmHubProxy.js +1 -378
- package/dist/router/headerAnalyzer.d.ts +14 -18
- package/dist/router/headerAnalyzer.d.ts.map +1 -1
- package/dist/router/headerAnalyzer.js +40 -67
- 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 -14
- package/dist/router/directCloudRouter.d.ts +0 -32
- package/dist/router/directCloudRouter.d.ts.map +0 -1
- package/dist/router/directCloudRouter.js +0 -179
- package/dist/router/localBasicRouter.d.ts +0 -23
- package/dist/router/localBasicRouter.d.ts.map +0 -1
- package/dist/router/localBasicRouter.js +0 -140
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,55 +315,187 @@ 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
|
|
331
426
|
};
|
|
332
|
-
|
|
333
|
-
|
|
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,
|
|
450
|
+
};
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
481
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
482
|
+
logger_js_1.logger?.error('Failed to handle BtpProxy request', {
|
|
483
|
+
type: 'BTP_PROXY_ERROR',
|
|
484
|
+
error: errorMessage,
|
|
347
485
|
stack: error instanceof Error ? error.stack : undefined,
|
|
348
486
|
});
|
|
487
|
+
// Output error to stderr for user visibility (only if verbose mode is enabled)
|
|
488
|
+
if ((0, btpProxy_js_1.shouldWriteStderr)()) {
|
|
489
|
+
process.stderr.write(`[MCP Proxy] ✗ Connection error: ${errorMessage}\n`);
|
|
490
|
+
}
|
|
349
491
|
if (!res.headersSent) {
|
|
350
|
-
res.writeHead(500, {
|
|
492
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
351
493
|
res.end(JSON.stringify({
|
|
352
|
-
jsonrpc:
|
|
353
|
-
id: intercepted.body
|
|
494
|
+
jsonrpc: '2.0',
|
|
495
|
+
id: getBodyId(intercepted.body) || null,
|
|
354
496
|
error: {
|
|
355
497
|
code: -32000,
|
|
356
|
-
message:
|
|
498
|
+
message: errorMessage,
|
|
357
499
|
},
|
|
358
500
|
}));
|
|
359
501
|
}
|
|
@@ -365,36 +507,38 @@ class McpAbapAdtProxyServer {
|
|
|
365
507
|
async startSseServer() {
|
|
366
508
|
const sseConfig = this.transportConfig;
|
|
367
509
|
const streamPathMap = new Map([
|
|
368
|
-
[
|
|
369
|
-
[
|
|
370
|
-
[
|
|
510
|
+
['/', '/messages'],
|
|
511
|
+
['/mcp/events', '/mcp/messages'],
|
|
512
|
+
['/sse', '/messages'],
|
|
371
513
|
]);
|
|
372
|
-
const
|
|
514
|
+
const _streamPaths = Array.from(streamPathMap.keys());
|
|
373
515
|
const postPathSet = new Set(streamPathMap.values());
|
|
374
|
-
postPathSet.add(
|
|
375
|
-
postPathSet.add(
|
|
376
|
-
const httpServer = (0,
|
|
516
|
+
postPathSet.add('/messages');
|
|
517
|
+
postPathSet.add('/mcp/messages');
|
|
518
|
+
const httpServer = (0, node_http_1.createServer)(async (req, res) => {
|
|
377
519
|
// SSE: Always restrict to local connections only
|
|
378
520
|
const remoteAddress = req.socket.remoteAddress;
|
|
379
|
-
const isLocal = remoteAddress ===
|
|
380
|
-
remoteAddress ===
|
|
381
|
-
remoteAddress ===
|
|
382
|
-
|
|
521
|
+
const isLocal = remoteAddress === '127.0.0.1' ||
|
|
522
|
+
remoteAddress === '::1' ||
|
|
523
|
+
remoteAddress === 'localhost' ||
|
|
524
|
+
remoteAddress?.startsWith('127.');
|
|
383
525
|
if (!isLocal) {
|
|
384
|
-
logger_js_1.logger
|
|
385
|
-
type:
|
|
526
|
+
logger_js_1.logger?.warn('SSE: Non-local connection rejected', {
|
|
527
|
+
type: 'SSE_NON_LOCAL_REJECTED',
|
|
386
528
|
remoteAddress,
|
|
387
529
|
});
|
|
388
|
-
res.writeHead(403, {
|
|
389
|
-
res.end(
|
|
530
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
531
|
+
res.end('Forbidden: SSE transport only accepts local connections');
|
|
390
532
|
return;
|
|
391
533
|
}
|
|
392
|
-
const requestUrl = req.url
|
|
393
|
-
|
|
394
|
-
|
|
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('/')) {
|
|
395
539
|
pathname = pathname.slice(0, -1);
|
|
396
540
|
}
|
|
397
|
-
// Apply config overrides (--btp
|
|
541
|
+
// Apply config overrides (--btp) to headers if not present
|
|
398
542
|
const headers = {};
|
|
399
543
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
400
544
|
if (value) {
|
|
@@ -402,33 +546,27 @@ class McpAbapAdtProxyServer {
|
|
|
402
546
|
}
|
|
403
547
|
}
|
|
404
548
|
// Add config overrides if not present in headers
|
|
405
|
-
if (this.config.btpDestination && !headers[
|
|
406
|
-
headers[
|
|
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;
|
|
549
|
+
if (this.config.btpDestination && !headers[interfaces_1.HEADER_BTP_DESTINATION]) {
|
|
550
|
+
headers[interfaces_1.HEADER_BTP_DESTINATION] = this.config.btpDestination;
|
|
413
551
|
}
|
|
414
|
-
logger_js_1.logger
|
|
415
|
-
type:
|
|
552
|
+
logger_js_1.logger?.debug('SSE request received', {
|
|
553
|
+
type: 'SSE_HTTP_REQUEST',
|
|
416
554
|
method: req.method,
|
|
417
555
|
pathname,
|
|
418
556
|
originalUrl: req.url,
|
|
419
557
|
});
|
|
420
558
|
// GET /sse, /mcp/events, or / - establish SSE connection
|
|
421
|
-
if (req.method ===
|
|
422
|
-
const postEndpoint = streamPathMap.get(pathname) ??
|
|
423
|
-
logger_js_1.logger
|
|
424
|
-
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',
|
|
425
563
|
pathname,
|
|
426
564
|
postEndpoint,
|
|
427
565
|
});
|
|
428
566
|
// Create new McpServer instance for this session
|
|
429
567
|
const sessionServer = new mcp_js_1.McpServer({
|
|
430
|
-
name:
|
|
431
|
-
version:
|
|
568
|
+
name: '@mcp-abap-adt/proxy',
|
|
569
|
+
version: '0.1.0',
|
|
432
570
|
}, {
|
|
433
571
|
capabilities: {
|
|
434
572
|
tools: {},
|
|
@@ -441,29 +579,29 @@ class McpAbapAdtProxyServer {
|
|
|
441
579
|
enableDnsRebindingProtection: sseConfig.enableDnsRebindingProtection,
|
|
442
580
|
});
|
|
443
581
|
const sessionId = transport.sessionId;
|
|
444
|
-
logger_js_1.logger
|
|
445
|
-
type:
|
|
582
|
+
logger_js_1.logger?.info('New SSE session created', {
|
|
583
|
+
type: 'SSE_SESSION_CREATED',
|
|
446
584
|
sessionId,
|
|
447
585
|
pathname,
|
|
448
586
|
});
|
|
449
587
|
// Connect transport to server
|
|
450
588
|
try {
|
|
451
589
|
await sessionServer.server.connect(transport);
|
|
452
|
-
logger_js_1.logger
|
|
453
|
-
type:
|
|
590
|
+
logger_js_1.logger?.info('SSE transport connected', {
|
|
591
|
+
type: 'SSE_CONNECTION_READY',
|
|
454
592
|
sessionId,
|
|
455
593
|
pathname,
|
|
456
594
|
postEndpoint,
|
|
457
595
|
});
|
|
458
596
|
}
|
|
459
597
|
catch (error) {
|
|
460
|
-
logger_js_1.logger
|
|
461
|
-
type:
|
|
598
|
+
logger_js_1.logger?.error('Failed to connect SSE transport', {
|
|
599
|
+
type: 'SSE_CONNECT_ERROR',
|
|
462
600
|
error: error instanceof Error ? error.message : String(error),
|
|
463
601
|
sessionId,
|
|
464
602
|
});
|
|
465
603
|
if (!res.headersSent) {
|
|
466
|
-
res.writeHead(500).end(
|
|
604
|
+
res.writeHead(500).end('Internal Server Error');
|
|
467
605
|
}
|
|
468
606
|
else {
|
|
469
607
|
res.end();
|
|
@@ -471,17 +609,17 @@ class McpAbapAdtProxyServer {
|
|
|
471
609
|
return;
|
|
472
610
|
}
|
|
473
611
|
// Cleanup on connection close
|
|
474
|
-
res.on(
|
|
475
|
-
logger_js_1.logger
|
|
476
|
-
type:
|
|
612
|
+
res.on('close', () => {
|
|
613
|
+
logger_js_1.logger?.info('SSE connection closed', {
|
|
614
|
+
type: 'SSE_CONNECTION_CLOSED',
|
|
477
615
|
sessionId,
|
|
478
616
|
pathname,
|
|
479
617
|
});
|
|
480
618
|
sessionServer.server.close();
|
|
481
619
|
});
|
|
482
620
|
transport.onerror = (error) => {
|
|
483
|
-
logger_js_1.logger
|
|
484
|
-
type:
|
|
621
|
+
logger_js_1.logger?.error('SSE transport error', {
|
|
622
|
+
type: 'SSE_TRANSPORT_ERROR',
|
|
485
623
|
error: error instanceof Error ? error.message : String(error),
|
|
486
624
|
sessionId,
|
|
487
625
|
});
|
|
@@ -489,30 +627,30 @@ class McpAbapAdtProxyServer {
|
|
|
489
627
|
return;
|
|
490
628
|
}
|
|
491
629
|
// POST /messages or /mcp/messages - handle client messages
|
|
492
|
-
if (req.method ===
|
|
630
|
+
if (req.method === 'POST' && postPathSet.has(pathname)) {
|
|
493
631
|
// Extract sessionId from query string or header
|
|
494
632
|
let sessionId;
|
|
495
633
|
if (requestUrl) {
|
|
496
|
-
sessionId = requestUrl.searchParams.get(
|
|
634
|
+
sessionId = requestUrl.searchParams.get('sessionId') || undefined;
|
|
497
635
|
}
|
|
498
636
|
if (!sessionId) {
|
|
499
|
-
sessionId = req.headers[
|
|
637
|
+
sessionId = req.headers['x-session-id'];
|
|
500
638
|
}
|
|
501
|
-
logger_js_1.logger
|
|
502
|
-
type:
|
|
639
|
+
logger_js_1.logger?.debug('SSE POST request received', {
|
|
640
|
+
type: 'SSE_POST_REQUEST',
|
|
503
641
|
sessionId,
|
|
504
642
|
pathname,
|
|
505
643
|
});
|
|
506
644
|
if (!sessionId) {
|
|
507
|
-
logger_js_1.logger
|
|
508
|
-
type:
|
|
645
|
+
logger_js_1.logger?.error('Missing sessionId in SSE POST request', {
|
|
646
|
+
type: 'SSE_MISSING_SESSION_ID',
|
|
509
647
|
pathname,
|
|
510
648
|
});
|
|
511
|
-
res.writeHead(400, {
|
|
512
|
-
jsonrpc:
|
|
649
|
+
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({
|
|
650
|
+
jsonrpc: '2.0',
|
|
513
651
|
error: {
|
|
514
652
|
code: -32000,
|
|
515
|
-
message:
|
|
653
|
+
message: 'Missing sessionId',
|
|
516
654
|
},
|
|
517
655
|
id: null,
|
|
518
656
|
}));
|
|
@@ -526,20 +664,20 @@ class McpAbapAdtProxyServer {
|
|
|
526
664
|
chunks.push(chunk);
|
|
527
665
|
}
|
|
528
666
|
if (chunks.length > 0) {
|
|
529
|
-
const bodyString = Buffer.concat(chunks).toString(
|
|
667
|
+
const bodyString = Buffer.concat(chunks).toString('utf-8');
|
|
530
668
|
body = JSON.parse(bodyString);
|
|
531
669
|
}
|
|
532
670
|
}
|
|
533
671
|
catch (error) {
|
|
534
|
-
logger_js_1.logger
|
|
535
|
-
type:
|
|
672
|
+
logger_js_1.logger?.error('Failed to parse SSE POST request body', {
|
|
673
|
+
type: 'SSE_POST_PARSE_ERROR',
|
|
536
674
|
error: error instanceof Error ? error.message : String(error),
|
|
537
675
|
});
|
|
538
|
-
res.writeHead(400, {
|
|
539
|
-
jsonrpc:
|
|
676
|
+
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({
|
|
677
|
+
jsonrpc: '2.0',
|
|
540
678
|
error: {
|
|
541
679
|
code: -32700,
|
|
542
|
-
message:
|
|
680
|
+
message: 'Parse error',
|
|
543
681
|
},
|
|
544
682
|
id: null,
|
|
545
683
|
}));
|
|
@@ -548,19 +686,17 @@ class McpAbapAdtProxyServer {
|
|
|
548
686
|
// Intercept and analyze request with config overrides
|
|
549
687
|
const configOverrides = {
|
|
550
688
|
btpDestination: this.config.btpDestination,
|
|
551
|
-
mcpDestination: this.config.mcpDestination,
|
|
552
|
-
mcpUrl: this.config.mcpUrl,
|
|
553
689
|
};
|
|
554
690
|
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
|
|
555
691
|
// Check routing decision
|
|
556
692
|
if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
|
|
557
|
-
logger_js_1.logger
|
|
558
|
-
type:
|
|
693
|
+
logger_js_1.logger?.error('Routing decision failed for SSE POST', {
|
|
694
|
+
type: 'SSE_POST_ROUTING_FAILED',
|
|
559
695
|
reason: intercepted.routingDecision.reason,
|
|
560
696
|
});
|
|
561
|
-
res.writeHead(400, {
|
|
562
|
-
jsonrpc:
|
|
563
|
-
id: body
|
|
697
|
+
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({
|
|
698
|
+
jsonrpc: '2.0',
|
|
699
|
+
id: getBodyId(body) || null,
|
|
564
700
|
error: {
|
|
565
701
|
code: -32602,
|
|
566
702
|
message: intercepted.routingDecision.reason,
|
|
@@ -573,18 +709,18 @@ class McpAbapAdtProxyServer {
|
|
|
573
709
|
await this.handleProxyRequest(intercepted, req, res);
|
|
574
710
|
}
|
|
575
711
|
catch (error) {
|
|
576
|
-
logger_js_1.logger
|
|
577
|
-
type:
|
|
712
|
+
logger_js_1.logger?.error('Failed to process SSE POST request', {
|
|
713
|
+
type: 'SSE_POST_PROCESS_ERROR',
|
|
578
714
|
error: error instanceof Error ? error.message : String(error),
|
|
579
715
|
});
|
|
580
716
|
if (!res.headersSent) {
|
|
581
|
-
res.writeHead(500, {
|
|
717
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
582
718
|
res.end(JSON.stringify({
|
|
583
|
-
jsonrpc:
|
|
584
|
-
id: body
|
|
719
|
+
jsonrpc: '2.0',
|
|
720
|
+
id: getBodyId(body) || null,
|
|
585
721
|
error: {
|
|
586
722
|
code: -32000,
|
|
587
|
-
message: error instanceof Error ? error.message :
|
|
723
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
588
724
|
},
|
|
589
725
|
}));
|
|
590
726
|
}
|
|
@@ -592,25 +728,31 @@ class McpAbapAdtProxyServer {
|
|
|
592
728
|
return;
|
|
593
729
|
}
|
|
594
730
|
// Unknown path or method
|
|
595
|
-
res.writeHead(404, {
|
|
596
|
-
res.end(
|
|
731
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
732
|
+
res.end('Not found');
|
|
597
733
|
});
|
|
598
734
|
const port = sseConfig.port || 3002;
|
|
599
|
-
const host = sseConfig.host ||
|
|
735
|
+
const host = sseConfig.host || '0.0.0.0';
|
|
600
736
|
return new Promise((resolve, reject) => {
|
|
601
737
|
httpServer.listen(port, host, () => {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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',
|
|
605
747
|
host,
|
|
606
748
|
port,
|
|
607
749
|
});
|
|
608
750
|
this.httpServer = httpServer;
|
|
609
751
|
resolve();
|
|
610
752
|
});
|
|
611
|
-
httpServer.on(
|
|
612
|
-
logger_js_1.logger
|
|
613
|
-
type:
|
|
753
|
+
httpServer.on('error', (error) => {
|
|
754
|
+
logger_js_1.logger?.error('Failed to start SSE server', {
|
|
755
|
+
type: 'SERVER_START_ERROR',
|
|
614
756
|
error: error.message,
|
|
615
757
|
});
|
|
616
758
|
reject(error);
|
|
@@ -625,15 +767,15 @@ class McpAbapAdtProxyServer {
|
|
|
625
767
|
await this.server.close();
|
|
626
768
|
}
|
|
627
769
|
catch (error) {
|
|
628
|
-
logger_js_1.logger
|
|
629
|
-
type:
|
|
770
|
+
logger_js_1.logger?.error('Failed to close MCP server', {
|
|
771
|
+
type: 'SERVER_SHUTDOWN_ERROR',
|
|
630
772
|
error: error instanceof Error ? error.message : String(error),
|
|
631
773
|
});
|
|
632
774
|
}
|
|
633
775
|
if (this.httpServer) {
|
|
634
776
|
return new Promise((resolve) => {
|
|
635
|
-
this.httpServer
|
|
636
|
-
logger_js_1.logger
|
|
777
|
+
this.httpServer?.close(() => {
|
|
778
|
+
logger_js_1.logger?.info('HTTP server closed');
|
|
637
779
|
resolve();
|
|
638
780
|
});
|
|
639
781
|
});
|
|
@@ -643,3 +785,28 @@ class McpAbapAdtProxyServer {
|
|
|
643
785
|
exports.McpAbapAdtProxyServer = McpAbapAdtProxyServer;
|
|
644
786
|
// Export for use in bin script
|
|
645
787
|
exports.default = McpAbapAdtProxyServer;
|
|
788
|
+
// Auto-start server when run directly (unless MCP_SKIP_AUTO_START is set)
|
|
789
|
+
if (process.env.MCP_SKIP_AUTO_START !== 'true') {
|
|
790
|
+
const server = new McpAbapAdtProxyServer();
|
|
791
|
+
server.run().catch((error) => {
|
|
792
|
+
logger_js_1.logger?.error('Fatal error while running MCP proxy server', {
|
|
793
|
+
type: 'SERVER_FATAL_ERROR',
|
|
794
|
+
error: error instanceof Error ? error.message : String(error),
|
|
795
|
+
});
|
|
796
|
+
// Always write fatal errors to stderr (even in non-verbose mode, as these are critical)
|
|
797
|
+
// But skip in test environment
|
|
798
|
+
const isTestEnv = process.env.NODE_ENV === 'test' ||
|
|
799
|
+
process.env.JEST_WORKER_ID !== undefined ||
|
|
800
|
+
typeof globalThis.jest !== 'undefined';
|
|
801
|
+
if (!isTestEnv) {
|
|
802
|
+
process.stderr.write(`[MCP Proxy] ✗ Fatal error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
803
|
+
}
|
|
804
|
+
// On Windows, add a small delay before exit to allow error message to be visible
|
|
805
|
+
if (process.platform === 'win32') {
|
|
806
|
+
setTimeout(() => process.exit(1), 100);
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
process.exit(1);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|