@mcp-abap-adt/proxy 0.1.8 → 0.2.0
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 +11 -1
- package/README.md +10 -1
- package/bin/mcp-abap-adt-proxy.js +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -276
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +33 -0
- package/dist/lib/stores.d.ts +14 -0
- package/dist/lib/stores.d.ts.map +1 -1
- package/dist/lib/stores.js +10 -1
- package/dist/lib/transportConfig.d.ts +15 -2
- package/dist/lib/transportConfig.d.ts.map +1 -1
- package/dist/lib/transportConfig.js +41 -33
- package/dist/proxy/btpProxy.d.ts +6 -1
- package/dist/proxy/btpProxy.d.ts.map +1 -1
- package/dist/proxy/btpProxy.js +16 -0
- package/dist/proxy/reverseProxy.d.ts +7 -0
- package/dist/proxy/reverseProxy.d.ts.map +1 -0
- package/dist/proxy/reverseProxy.js +130 -0
- package/dist/router/requestInterceptor.d.ts +2 -0
- package/dist/router/requestInterceptor.d.ts.map +1 -1
- package/dist/router/requestInterceptor.js +17 -15
- package/dist/tui/index.d.ts +8 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +11 -0
- package/dist/tui/serviceKeyCheck.d.ts +25 -0
- package/dist/tui/serviceKeyCheck.d.ts.map +1 -0
- package/dist/tui/serviceKeyCheck.js +62 -0
- package/dist/tui/wizard.d.ts +38 -0
- package/dist/tui/wizard.d.ts.map +1 -0
- package/dist/tui/wizard.js +321 -0
- package/package.json +15 -14
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.9] - 2026-02-12
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **AUTH_BROKER_PATH resolution aligned with `mcp-abap-adt`**
|
|
14
|
+
- `AUTH_BROKER_PATH` is now treated as a base directory.
|
|
15
|
+
- Proxy now resolves stores from `<base>/service-keys` and `<base>/sessions`.
|
|
16
|
+
- If a path already ends with `service-keys` or `sessions`, it is normalized and still resolved correctly.
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
- Clarified in README and troubleshooting docs how default store paths and `AUTH_BROKER_PATH` are resolved.
|
|
20
|
+
|
|
10
21
|
## [0.1.8] - 2026-02-05
|
|
11
22
|
|
|
12
23
|
### Added
|
|
@@ -198,4 +209,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
198
209
|
- `@mcp-abap-adt/header-validator` ^0.1.3 - Header validation
|
|
199
210
|
- `axios` - HTTP client
|
|
200
211
|
- `zod` - Schema validation
|
|
201
|
-
|
package/README.md
CHANGED
|
@@ -157,8 +157,17 @@ The proxy is transparent - it only adds authentication headers and forwards requ
|
|
|
157
157
|
export MCP_HTTP_PORT=3001
|
|
158
158
|
export LOG_LEVEL=info
|
|
159
159
|
export MCP_PROXY_UNSAFE=true # Enable file-based session storage (optional)
|
|
160
|
+
export AUTH_BROKER_PATH=~/.config/mcp-abap-adt # Optional base path for service-keys/sessions
|
|
160
161
|
```
|
|
161
162
|
|
|
163
|
+
`AUTH_BROKER_PATH` is treated as a base directory. The proxy resolves:
|
|
164
|
+
- `service-keys` from `<AUTH_BROKER_PATH>/service-keys`
|
|
165
|
+
- `sessions` from `<AUTH_BROKER_PATH>/sessions`
|
|
166
|
+
|
|
167
|
+
Defaults when `AUTH_BROKER_PATH` is not set:
|
|
168
|
+
- Unix/Linux/macOS: `~/.config/mcp-abap-adt/service-keys` and `~/.config/mcp-abap-adt/sessions`
|
|
169
|
+
- Windows: `%USERPROFILE%\\Documents\\mcp-abap-adt\\service-keys` and `%USERPROFILE%\\Documents\\mcp-abap-adt\\sessions`
|
|
170
|
+
|
|
162
171
|
### Configuration File
|
|
163
172
|
|
|
164
173
|
Create `mcp-proxy-config.json`:
|
|
@@ -175,7 +184,7 @@ Create `mcp-proxy-config.json`:
|
|
|
175
184
|
|
|
176
185
|
**Session Storage:**
|
|
177
186
|
- `unsafe: false` (default) - Session data stored in-memory (secure, lost on restart)
|
|
178
|
-
- `unsafe: true` - Session data persisted to disk (tokens saved
|
|
187
|
+
- `unsafe: true` - Session data persisted to disk (tokens saved under the session store path)
|
|
179
188
|
|
|
180
189
|
See [Configuration Guide](./docs/CONFIGURATION.md) for complete options.
|
|
181
190
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* mcp-abap-adt-proxy [options]
|
|
10
|
+
* mcp-abap-adt-proxy tui
|
|
10
11
|
* mcp-abap-adt-proxy --transport=stdio
|
|
11
12
|
* mcp-abap-adt-proxy --transport=http
|
|
12
13
|
*/
|
|
@@ -43,6 +44,10 @@ MCP ABAP ADT Proxy Server v${pkg.version}
|
|
|
43
44
|
|
|
44
45
|
Usage:
|
|
45
46
|
mcp-abap-adt-proxy [options]
|
|
47
|
+
mcp-abap-adt-proxy tui
|
|
48
|
+
|
|
49
|
+
Commands:
|
|
50
|
+
tui Interactive configuration wizard (generates YAML config)
|
|
46
51
|
|
|
47
52
|
Options:
|
|
48
53
|
--transport=<type> Transport type: stdio, http, streamable-http, sse
|
|
@@ -77,6 +82,7 @@ Examples:
|
|
|
77
82
|
mcp-abap-adt-proxy --transport=sse --sse-port=3002 # SSE mode on port 3002
|
|
78
83
|
mcp-abap-adt-proxy --btp=ai # With BTP destination override
|
|
79
84
|
mcp-abap-adt-proxy --btp=ai --unsafe # With file-based session storage
|
|
85
|
+
mcp-abap-adt-proxy tui # Interactive configuration wizard
|
|
80
86
|
mcp-abap-adt-proxy --config=proxy-config.yaml # Load configuration from YAML file
|
|
81
87
|
mcp-abap-adt-proxy -c proxy-config.yml # Load configuration from YAML file (short form)
|
|
82
88
|
|
|
@@ -137,6 +143,30 @@ function main() {
|
|
|
137
143
|
process.exit(0);
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
// Handle 'tui' subcommand — interactive configuration wizard
|
|
147
|
+
if (process.argv[2] === 'tui') {
|
|
148
|
+
const wizardPath = path.resolve(__dirname, '../dist/tui/index.js');
|
|
149
|
+
if (!fs.existsSync(wizardPath)) {
|
|
150
|
+
process.stderr.write(`[MCP Proxy] ✗ Wizard not found at: ${wizardPath}\n`);
|
|
151
|
+
process.stderr.write(`[MCP Proxy] Make sure to build the project with 'npm run build' first.\n`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
import(wizardPath).then(({ runWizard }) => {
|
|
156
|
+
runWizard().catch((err) => {
|
|
157
|
+
if (err.name === 'ExitPromptError') {
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
process.stderr.write(`[MCP Proxy] ✗ Wizard error: ${err.message}\n`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
163
|
+
}).catch((err) => {
|
|
164
|
+
process.stderr.write(`[MCP Proxy] ✗ Failed to load wizard: ${err.message}\n`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
140
170
|
// Spawn the main server process
|
|
141
171
|
const serverPath = path.resolve(__dirname, '../dist/index.js');
|
|
142
172
|
|
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare class McpAbapAdtProxyServer {
|
|
|
31
31
|
*/
|
|
32
32
|
private startHttpServer;
|
|
33
33
|
/**
|
|
34
|
-
* Handle proxy request -
|
|
34
|
+
* Handle proxy request - get JWT token and forward transparently via reverse proxy
|
|
35
35
|
*/
|
|
36
36
|
private handleProxyRequest;
|
|
37
37
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAuBH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAqBlC;;GAEG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,CAAW;gBAEhB,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAoDlE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAuE1B;;OAEG;YACW,eAAe;IAgG7B;;OAEG;YACW,kBAAkB;IA8ChC;;OAEG;YACW,cAAc;IA2T5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAmBhC;AAGD,eAAe,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const interfaces_1 = require("@mcp-abap-adt/interfaces");
|
|
|
15
15
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
16
16
|
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
17
17
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
18
|
+
const reverseProxy_js_1 = require("./proxy/reverseProxy.js");
|
|
18
19
|
const config_js_1 = require("./lib/config.js");
|
|
19
20
|
const logger_js_1 = require("./lib/logger.js");
|
|
20
21
|
const transportConfig_js_1 = require("./lib/transportConfig.js");
|
|
@@ -40,7 +41,17 @@ class McpAbapAdtProxyServer {
|
|
|
40
41
|
httpServer;
|
|
41
42
|
btpProxy;
|
|
42
43
|
constructor(transportConfig, configPath) {
|
|
43
|
-
|
|
44
|
+
// Load config first to extract transport overrides from YAML/JSON file
|
|
45
|
+
const resolvedConfigPath = configPath || (0, config_js_1.getConfigPath)();
|
|
46
|
+
let fileTransportOverrides;
|
|
47
|
+
if (resolvedConfigPath) {
|
|
48
|
+
const raw = (0, config_js_1.loadRawConfigFile)(resolvedConfigPath);
|
|
49
|
+
if (raw) {
|
|
50
|
+
fileTransportOverrides = (0, config_js_1.extractTransportFields)(raw);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.transportConfig =
|
|
54
|
+
transportConfig || (0, transportConfig_js_1.parseTransportConfig)(fileTransportOverrides);
|
|
44
55
|
this.config = (0, config_js_1.loadConfig)(configPath);
|
|
45
56
|
// Validate configuration
|
|
46
57
|
if (this.transportConfig.type === 'streamable-http' ||
|
|
@@ -145,9 +156,7 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
145
156
|
async startHttpServer() {
|
|
146
157
|
const httpConfig = this.transportConfig;
|
|
147
158
|
this.httpServer = (0, node_http_1.createServer)(async (req, res) => {
|
|
148
|
-
// Log incoming HTTP request
|
|
149
|
-
const _debugEnabled = process.env.DEBUG === 'true' ||
|
|
150
|
-
process.env.DEBUG_HTTP_REQUESTS === 'true';
|
|
159
|
+
// Log incoming HTTP request
|
|
151
160
|
const sanitizedHeaders = (0, requestInterceptor_js_1.sanitizeHeadersForLogging)(req.headers);
|
|
152
161
|
logger_js_1.logger?.info('=== HTTP REQUEST RECEIVED ===', {
|
|
153
162
|
type: 'HTTP_REQUEST_RECEIVED',
|
|
@@ -156,92 +165,14 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
156
165
|
headers: sanitizedHeaders,
|
|
157
166
|
remoteAddress: req.socket.remoteAddress,
|
|
158
167
|
});
|
|
159
|
-
//
|
|
160
|
-
// if (req.method !== 'POST') {
|
|
161
|
-
// logger?.warn('Non-POST request rejected', { ... });
|
|
162
|
-
// ...
|
|
163
|
-
// }
|
|
164
|
-
// Read request body
|
|
165
|
-
let body;
|
|
166
|
-
try {
|
|
167
|
-
const chunks = [];
|
|
168
|
-
for await (const chunk of req) {
|
|
169
|
-
chunks.push(chunk);
|
|
170
|
-
}
|
|
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
|
-
}
|
|
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
|
-
errorStack: error instanceof Error ? error.stack : undefined,
|
|
230
|
-
});
|
|
231
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
232
|
-
res.end('Invalid request body');
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
// Intercept and analyze request
|
|
168
|
+
// Intercept and analyze request (headers only, body is piped through)
|
|
236
169
|
const configOverrides = {
|
|
237
170
|
btpDestination: this.config.btpDestination,
|
|
238
171
|
targetUrl: this.config.targetUrl,
|
|
239
172
|
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
configOverrides,
|
|
173
|
+
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, undefined, configOverrides, {
|
|
174
|
+
skipHeaderValidation: true,
|
|
243
175
|
});
|
|
244
|
-
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
|
|
245
176
|
// Check routing decision
|
|
246
177
|
if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
|
|
247
178
|
logger_js_1.logger?.error('Routing decision failed', {
|
|
@@ -249,25 +180,16 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
249
180
|
reason: intercepted.routingDecision.reason,
|
|
250
181
|
});
|
|
251
182
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
252
|
-
const bodyId = body && typeof body === 'object' && body !== null && 'id' in body
|
|
253
|
-
? body.id
|
|
254
|
-
: null;
|
|
255
183
|
res.end(JSON.stringify({
|
|
256
|
-
|
|
257
|
-
id: bodyId || null,
|
|
258
|
-
error: {
|
|
259
|
-
code: -32602,
|
|
260
|
-
message: intercepted.routingDecision.reason,
|
|
261
|
-
},
|
|
184
|
+
error: intercepted.routingDecision.reason,
|
|
262
185
|
}));
|
|
263
186
|
return;
|
|
264
187
|
}
|
|
265
|
-
//
|
|
188
|
+
// Forward request via reverse proxy
|
|
266
189
|
logger_js_1.logger?.info('=== PROXYING REQUEST ===', {
|
|
267
190
|
type: 'PROXY_REQUEST_START',
|
|
268
191
|
btpDestination: intercepted.routingDecision.btpDestination,
|
|
269
192
|
});
|
|
270
|
-
// Handle proxy request - add JWT and forward to x-mcp-url
|
|
271
193
|
try {
|
|
272
194
|
await this.handleProxyRequest(intercepted, req, res);
|
|
273
195
|
}
|
|
@@ -278,8 +200,8 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
278
200
|
strategy: intercepted.routingDecision.strategy,
|
|
279
201
|
});
|
|
280
202
|
if (!res.headersSent) {
|
|
281
|
-
res.writeHead(500, { 'Content-Type': '
|
|
282
|
-
res.end('Internal server error');
|
|
203
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
204
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
283
205
|
}
|
|
284
206
|
}
|
|
285
207
|
});
|
|
@@ -311,192 +233,38 @@ Authorization source: ${authSourceObj}`;
|
|
|
311
233
|
});
|
|
312
234
|
}
|
|
313
235
|
/**
|
|
314
|
-
* Handle proxy request -
|
|
236
|
+
* Handle proxy request - get JWT token and forward transparently via reverse proxy
|
|
315
237
|
*/
|
|
316
238
|
async handleProxyRequest(intercepted, req, res) {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
}
|
|
239
|
+
const destination = intercepted.routingDecision.btpDestination;
|
|
240
|
+
if (!destination) {
|
|
241
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
242
|
+
res.end(JSON.stringify({ error: 'No BTP destination specified' }));
|
|
243
|
+
return;
|
|
333
244
|
}
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
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;
|
|
245
|
+
// Ensure proxy is initialized
|
|
246
|
+
if (!this.btpProxy) {
|
|
247
|
+
this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
|
|
373
248
|
}
|
|
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,
|
|
381
|
-
btpDestination,
|
|
382
|
-
sessionId: intercepted.sessionId,
|
|
383
|
-
headers: incomingHeaders,
|
|
384
|
-
body: sanitizedBody,
|
|
385
|
-
mode: 'BTP authentication',
|
|
386
|
-
});
|
|
387
249
|
try {
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
//
|
|
394
|
-
|
|
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,
|
|
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
|
-
});
|
|
467
|
-
// Send response back to client
|
|
468
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
469
|
-
res.end(JSON.stringify(proxyResponse));
|
|
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',
|
|
476
|
-
hasResult: !!proxyResponse.result,
|
|
477
|
-
hasError: !!proxyResponse.error,
|
|
478
|
-
});
|
|
250
|
+
// Get JWT token (cached, auto-refresh)
|
|
251
|
+
const jwtToken = await this.btpProxy.getJwtToken(destination);
|
|
252
|
+
// Get target URL
|
|
253
|
+
const targetUrl = intercepted.routingDecision.targetUrl ||
|
|
254
|
+
(await this.btpProxy.getTargetUrl(destination));
|
|
255
|
+
// Forward request transparently
|
|
256
|
+
await (0, reverseProxy_js_1.forwardRequest)(req, res, targetUrl, jwtToken);
|
|
479
257
|
}
|
|
480
258
|
catch (error) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
error:
|
|
485
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
259
|
+
logger_js_1.logger?.error('Proxy request failed', {
|
|
260
|
+
type: 'PROXY_REQUEST_ERROR',
|
|
261
|
+
destination,
|
|
262
|
+
error: error instanceof Error ? error.message : String(error),
|
|
486
263
|
});
|
|
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
|
-
}
|
|
491
264
|
if (!res.headersSent) {
|
|
492
|
-
res.writeHead(
|
|
265
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
493
266
|
res.end(JSON.stringify({
|
|
494
|
-
|
|
495
|
-
id: getBodyId(intercepted.body) || null,
|
|
496
|
-
error: {
|
|
497
|
-
code: -32000,
|
|
498
|
-
message: errorMessage,
|
|
499
|
-
},
|
|
267
|
+
error: error instanceof Error ? error.message : 'Proxy error',
|
|
500
268
|
}));
|
|
501
269
|
}
|
|
502
270
|
}
|
|
@@ -686,6 +454,7 @@ Authorization source: ${authSourceObj}`;
|
|
|
686
454
|
// Intercept and analyze request with config overrides
|
|
687
455
|
const configOverrides = {
|
|
688
456
|
btpDestination: this.config.btpDestination,
|
|
457
|
+
targetUrl: this.config.targetUrl,
|
|
689
458
|
};
|
|
690
459
|
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
|
|
691
460
|
// Check routing decision
|
|
@@ -704,9 +473,19 @@ Authorization source: ${authSourceObj}`;
|
|
|
704
473
|
}));
|
|
705
474
|
return;
|
|
706
475
|
}
|
|
707
|
-
// Handle proxy request
|
|
476
|
+
// Handle proxy request via BtpProxy (JSON-RPC mode for SSE)
|
|
708
477
|
try {
|
|
709
|
-
|
|
478
|
+
if (!this.btpProxy) {
|
|
479
|
+
this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
|
|
480
|
+
}
|
|
481
|
+
const proxyResponse = await this.btpProxy.proxyRequest({
|
|
482
|
+
method: intercepted.method,
|
|
483
|
+
url: intercepted.url,
|
|
484
|
+
data: body,
|
|
485
|
+
id: getBodyId(body),
|
|
486
|
+
}, intercepted.routingDecision, intercepted.headers);
|
|
487
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
488
|
+
res.end(JSON.stringify(proxyResponse));
|
|
710
489
|
}
|
|
711
490
|
catch (error) {
|
|
712
491
|
logger_js_1.logger?.error('Failed to process SSE POST request', {
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -27,6 +27,24 @@ export interface ProxyConfig {
|
|
|
27
27
|
* - If --config is NOT provided, load ONLY from command line parameters and environment variables
|
|
28
28
|
*/
|
|
29
29
|
export declare function loadConfig(configPath?: string): ProxyConfig;
|
|
30
|
+
/**
|
|
31
|
+
* Transport-related fields from config file (not part of ProxyConfig but needed by transportConfig)
|
|
32
|
+
*/
|
|
33
|
+
export interface FileTransportFields {
|
|
34
|
+
transport?: string;
|
|
35
|
+
httpPort?: number;
|
|
36
|
+
httpHost?: string;
|
|
37
|
+
ssePort?: number;
|
|
38
|
+
sseHost?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extract transport-related fields from a raw config object
|
|
42
|
+
*/
|
|
43
|
+
export declare function extractTransportFields(raw: Record<string, unknown>): FileTransportFields;
|
|
44
|
+
/**
|
|
45
|
+
* Load raw config from file for transport field extraction
|
|
46
|
+
*/
|
|
47
|
+
export declare function loadRawConfigFile(filePath: string): Record<string, unknown> | null;
|
|
30
48
|
/**
|
|
31
49
|
* Get config file path from command line (--config or -c)
|
|
32
50
|
*/
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,WAAW;IAC1B,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,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,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;IAG/B,OAAO,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAqC3D;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,WAAW;IAC1B,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,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,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;IAG/B,OAAO,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAqC3D;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,mBAAmB,CAQrB;AAgBD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAYhC;AA0GD;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAElD;AAUD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG;IACnD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAoDA"}
|
package/dist/lib/config.js
CHANGED
|
@@ -37,6 +37,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.loadConfig = loadConfig;
|
|
40
|
+
exports.extractTransportFields = extractTransportFields;
|
|
41
|
+
exports.loadRawConfigFile = loadRawConfigFile;
|
|
40
42
|
exports.getConfigPath = getConfigPath;
|
|
41
43
|
exports.validateConfig = validateConfig;
|
|
42
44
|
const fs = __importStar(require("node:fs"));
|
|
@@ -80,6 +82,18 @@ function loadConfig(configPath) {
|
|
|
80
82
|
// No --config provided: load ONLY from command line parameters and environment variables
|
|
81
83
|
return loadFromEnv();
|
|
82
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract transport-related fields from a raw config object
|
|
87
|
+
*/
|
|
88
|
+
function extractTransportFields(raw) {
|
|
89
|
+
return {
|
|
90
|
+
transport: typeof raw.transport === 'string' ? raw.transport : undefined,
|
|
91
|
+
httpPort: typeof raw.httpPort === 'number' ? raw.httpPort : undefined,
|
|
92
|
+
httpHost: typeof raw.httpHost === 'string' ? raw.httpHost : undefined,
|
|
93
|
+
ssePort: typeof raw.ssePort === 'number' ? raw.ssePort : undefined,
|
|
94
|
+
sseHost: typeof raw.sseHost === 'string' ? raw.sseHost : undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
83
97
|
/**
|
|
84
98
|
* Load configuration from file (supports JSON and YAML)
|
|
85
99
|
*/
|
|
@@ -93,6 +107,24 @@ function loadConfigFile(filePath) {
|
|
|
93
107
|
return JSON.parse(configContent);
|
|
94
108
|
}
|
|
95
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Load raw config from file for transport field extraction
|
|
112
|
+
*/
|
|
113
|
+
function loadRawConfigFile(filePath) {
|
|
114
|
+
try {
|
|
115
|
+
if (!fs.existsSync(filePath))
|
|
116
|
+
return null;
|
|
117
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
118
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
119
|
+
if (ext === '.yaml' || ext === '.yml') {
|
|
120
|
+
return yaml.load(content) || {};
|
|
121
|
+
}
|
|
122
|
+
return JSON.parse(content);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
96
128
|
/**
|
|
97
129
|
* Apply default values to partial config (used when loading from file)
|
|
98
130
|
*/
|
|
@@ -104,6 +136,7 @@ function applyDefaults(fileConfig) {
|
|
|
104
136
|
sseHost: fileConfig.sseHost || '0.0.0.0',
|
|
105
137
|
logLevel: fileConfig.logLevel || 'info',
|
|
106
138
|
btpDestination: fileConfig.btpDestination,
|
|
139
|
+
targetUrl: fileConfig.targetUrl,
|
|
107
140
|
unsafe: fileConfig.unsafe ?? false,
|
|
108
141
|
maxRetries: fileConfig.maxRetries ?? 3,
|
|
109
142
|
retryDelay: fileConfig.retryDelay ?? 1000,
|
package/dist/lib/stores.d.ts
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
* depending on the unsafe configuration parameter
|
|
5
5
|
*/
|
|
6
6
|
import type { IServiceKeyStore, ISessionStore } from '@mcp-abap-adt/auth-broker';
|
|
7
|
+
/**
|
|
8
|
+
* Get platform-specific default paths for service keys and sessions
|
|
9
|
+
*
|
|
10
|
+
* Priority (matching mcp-abap-adt logic):
|
|
11
|
+
* 1. AUTH_BROKER_PATH environment variable
|
|
12
|
+
* 2. Platform-specific standard paths (only if no env paths were found):
|
|
13
|
+
* - Unix: ~/.config/mcp-abap-adt/{subfolder}
|
|
14
|
+
* - Windows: %USERPROFILE%\Documents\mcp-abap-adt\{subfolder}
|
|
15
|
+
* 3. Current working directory (process.cwd())
|
|
16
|
+
*
|
|
17
|
+
* @param subfolder Subfolder name ('service-keys' or 'sessions')
|
|
18
|
+
* @returns Array of resolved absolute paths
|
|
19
|
+
*/
|
|
20
|
+
export declare function getPlatformPaths(subfolder?: 'service-keys' | 'sessions'): string[];
|
|
7
21
|
/**
|
|
8
22
|
* Get platform-specific stores
|
|
9
23
|
* Returns XSUAA stores for BTP authentication:
|
package/dist/lib/stores.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stores.d.ts","sourceRoot":"","sources":["../../src/lib/stores.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACd,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"stores.d.ts","sourceRoot":"","sources":["../../src/lib/stores.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACd,MAAM,2BAA2B,CAAC;AAOnC;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,CAAC,EAAE,cAAc,GAAG,UAAU,GACtC,MAAM,EAAE,CAgEV;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC;IACxE,eAAe,EAAE,gBAAgB,CAAC;IAClC,YAAY,EAAE,aAAa,CAAC;CAC7B,CAAC,CAqBD"}
|