@mcp-abap-adt/proxy 1.6.4 → 4.0.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/LICENSE +669 -17
  3. package/README.md +79 -8
  4. package/bin/mcp-abap-adt-proxy-mcp.js +113 -0
  5. package/dist/index.d.ts +10 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +54 -97
  8. package/dist/lib/stores.d.ts +23 -2
  9. package/dist/lib/stores.d.ts.map +1 -1
  10. package/dist/lib/stores.js +56 -1
  11. package/dist/mcp/cli.d.ts +2 -0
  12. package/dist/mcp/cli.d.ts.map +1 -0
  13. package/dist/mcp/cli.js +21 -0
  14. package/dist/mcp/configs.d.ts +33 -0
  15. package/dist/mcp/configs.d.ts.map +1 -0
  16. package/dist/mcp/configs.js +142 -0
  17. package/dist/mcp/ports.d.ts +15 -0
  18. package/dist/mcp/ports.d.ts.map +1 -0
  19. package/dist/mcp/ports.js +35 -0
  20. package/dist/mcp/registry.d.ts +57 -0
  21. package/dist/mcp/registry.d.ts.map +1 -0
  22. package/dist/mcp/registry.js +145 -0
  23. package/dist/mcp/server.d.ts +34 -0
  24. package/dist/mcp/server.d.ts.map +1 -0
  25. package/dist/mcp/server.js +79 -0
  26. package/dist/mcp/shutdown.d.ts +37 -0
  27. package/dist/mcp/shutdown.d.ts.map +1 -0
  28. package/dist/mcp/shutdown.js +82 -0
  29. package/dist/mcp/supervisor.d.ts +125 -0
  30. package/dist/mcp/supervisor.d.ts.map +1 -0
  31. package/dist/mcp/supervisor.js +330 -0
  32. package/dist/mcp/tools.d.ts +28 -0
  33. package/dist/mcp/tools.d.ts.map +1 -0
  34. package/dist/mcp/tools.js +152 -0
  35. package/dist/proxy/btpProxy.d.ts +24 -73
  36. package/dist/proxy/btpProxy.d.ts.map +1 -1
  37. package/dist/proxy/btpProxy.js +116 -631
  38. package/dist/proxy/credentials.d.ts +45 -0
  39. package/dist/proxy/credentials.d.ts.map +1 -0
  40. package/dist/proxy/credentials.js +41 -0
  41. package/dist/proxy/requestHandler.d.ts +38 -0
  42. package/dist/proxy/requestHandler.d.ts.map +1 -0
  43. package/dist/proxy/requestHandler.js +73 -0
  44. package/dist/proxy/reverseProxy.d.ts +11 -2
  45. package/dist/proxy/reverseProxy.d.ts.map +1 -1
  46. package/dist/proxy/reverseProxy.js +52 -9
  47. package/dist/router/headerAnalyzer.js +2 -2
  48. package/dist/router/requestInterceptor.js +9 -9
  49. package/docs/API.md +172 -0
  50. package/docs/ARCHITECTURE.md +322 -0
  51. package/docs/CLIENT_SETUP.md +413 -0
  52. package/docs/CONFIGURATION.md +258 -0
  53. package/docs/MIGRATION-4.0.md +125 -0
  54. package/docs/ROUTING_LOGIC.md +126 -0
  55. package/docs/TROUBLESHOOTING.md +488 -0
  56. package/docs/USAGE.md +422 -0
  57. package/docs/YAML_CONFIG.md +273 -0
  58. package/docs/mcp-proxy-config.example.yaml +62 -0
  59. package/package.json +17 -10
  60. package/dist/proxy/cloudLlmHubProxy.d.ts +0 -2
  61. package/dist/proxy/cloudLlmHubProxy.d.ts.map +0 -1
  62. package/dist/proxy/cloudLlmHubProxy.js +0 -3
package/README.md CHANGED
@@ -13,9 +13,10 @@ Enable local MCP clients to connect to remote MCP servers with automatic JWT tok
13
13
 
14
14
  ## Features
15
15
 
16
- - ✅ **JWT Token Management** - Automatic token retrieval, caching, and refresh via auth-broker
16
+ - ✅ **One shared credential** - the token is the auth-broker's business: it caches, knows expiry and renews behind the call. The proxy holds no token of its own
17
17
  - ✅ **Service Key Based** - MCP server URL is obtained from service key for BTP destination
18
- - ✅ **Error Handling** - Retry logic, circuit breaker, and comprehensive error handling
18
+ - ✅ **Transparent forwarding** - the client's headers go through as sent; the proxy answers for the `Authorization` header and nothing else
19
+ - ✅ **Error Handling** - token acquisition is retried with exponential backoff when the failure can get better; clear messages when it cannot
19
20
  - ✅ **Multiple Transport Modes** - HTTP, SSE, and stdio support
20
21
  - ✅ **Configuration Flexibility** - Environment variables, config files, or defaults
21
22
 
@@ -40,6 +41,58 @@ mcp-abap-adt-proxy --btp=ai
40
41
  mcp-abap-adt-proxy --btp=ai --unsafe
41
42
  ```
42
43
 
44
+ ## Two commands
45
+
46
+ The package installs two binaries, for two different jobs.
47
+
48
+ | Command | What it is for |
49
+ |---|---|
50
+ | `mcp-abap-adt-proxy` | **Be** a proxy. A client points at it and its requests are authenticated and forwarded. |
51
+ | `mcp-abap-adt-proxy-mcp` | **Manage** proxies. Speaks MCP over stdio; its tools start and stop proxies on demand. |
52
+
53
+ ### The management mode
54
+
55
+ Register it like any other MCP server:
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "abap-proxy": { "command": "mcp-abap-adt-proxy-mcp" }
61
+ }
62
+ }
63
+ ```
64
+
65
+ It works from the proxy configs you already keep:
66
+
67
+ ```
68
+ ~/.config/mcp-abap-adt/proxy/<name>.yaml (Windows: Documents\mcp-abap-adt\proxy\)
69
+ ```
70
+
71
+ These are the same files `mcp-abap-adt-proxy --config <file>` takes. Starting a
72
+ proxy is therefore **choosing a name**, not assembling settings — and
73
+ credentials stay in the config, resolved through `${VAR}` interpolation, rather
74
+ than travelling through a tool call.
75
+
76
+ | Tool | What it does |
77
+ |---|---|
78
+ | `proxy_configs` | Lists the configs available, by the name `proxy_start` takes. Call it first — the names cannot be guessed. |
79
+ | `proxy_start` | Starts a proxy from one of those configs and returns the URL it bound. The **port is not taken from the config**: a free one is bound instead. |
80
+ | `proxy_stop` | Stops a proxy this session started, freeing its port and releasing its credential. Proxies started by other sessions are never touched. |
81
+ | `proxy_status` | Lists this session's proxies and any others on this machine. Records whose process has died are pruned when read, so it cannot report a ghost. |
82
+
83
+ **The config name is the unit, not the destination.** Several configs commonly
84
+ name the same `btpDestination` and differ in target URL and headers — a
85
+ destination cannot tell them apart.
86
+
87
+ **Every proxy runs inside the management process.** Closing the session — or
88
+ `SIGINT`, or `SIGTERM` — stops all of them and frees their ports. This is
89
+ deliberate: a spawned child would be orphaned by any signal the parent did not
90
+ forward and would go on holding its HTTP and OAuth callback ports.
91
+
92
+ A proxy nobody has used for 30 minutes stops itself. That is a backstop for a
93
+ client that finished and forgot, not a substitute for `proxy_stop` — which is
94
+ why the tool descriptions say so, and say it again beside the URL.
95
+
43
96
  ### Configuration
44
97
 
45
98
  The proxy supports multiple configuration methods:
@@ -156,6 +209,8 @@ The proxy uses BTP/XSUAA authentication:
156
209
 
157
210
  ## Documentation
158
211
 
212
+ - 🚚 **[Migration to 4.0](./docs/MIGRATION-4.0.md)** — the contracts leave the umbrella, an SSE response streams and carries your headers, `getJwtToken()` is replaced, the circuit breaker is gone
213
+
159
214
  - **[Client Setup Guide](./docs/CLIENT_SETUP.md)** - Step-by-step setup for Cline and GitHub Copilot
160
215
  - **[Configuration Guide](./docs/CONFIGURATION.md)** - Complete configuration reference
161
216
  - **[YAML Configuration Guide](./docs/YAML_CONFIG.md)** - Using YAML/JSON configuration files
@@ -221,7 +276,6 @@ Create `mcp-proxy-config.json`:
221
276
  "httpPort": 3001,
222
277
  "logLevel": "info",
223
278
  "maxRetries": 3,
224
- "circuitBreakerThreshold": 5,
225
279
  "unsafe": false
226
280
  }
227
281
  ```
@@ -234,10 +288,9 @@ See [Configuration Guide](./docs/CONFIGURATION.md) for complete options.
234
288
 
235
289
  ## Error Handling & Resilience
236
290
 
237
- - **Retry Logic** - Exponential backoff for failed requests
238
- - **Circuit Breaker** - Prevents cascading failures
239
- - **Token Refresh** - Automatic token refresh on expiration
240
- - **Connection Pooling** - Efficient resource management
291
+ - **Retry Logic** - exponential backoff while getting a token, for failures that can get better (5xx, network). A missing service key is not one of them and fails at once, with a message naming the file to create
292
+ - **Token Refresh** - handled by the credential, which is asked per request and renews behind the call
293
+ - **No circuit breaker** - it guarded the buffered forward that 4.0.0 removed. The forwarding path now streams, and there is nowhere to put one without buffering the response again. `circuitBreakerThreshold` and `circuitBreakerTimeout` are still accepted so existing configs load, and do nothing
241
294
  - **Request Timeouts** - Configurable timeout handling
242
295
 
243
296
  ## Requirements
@@ -277,7 +330,25 @@ See [ROADMAP.md](./ROADMAP.md) for details.
277
330
 
278
331
  ## License
279
332
 
280
- MIT
333
+ **GNU General Public License v3.0 only** (`GPL-3.0-only`).
334
+ Earlier published versions were MIT and stay MIT — a licence change is not
335
+ retroactive.
336
+
337
+ Copyright © 2025–2026 Oleksii Kyslytsia
338
+
339
+ This program is free software: you can redistribute it and/or modify it under the
340
+ terms of the GNU General Public License as published by the Free Software
341
+ Foundation, version 3.
342
+
343
+ It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
344
+ without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
345
+ PURPOSE. See [`LICENSE`](LICENSE) for the full text.
346
+
347
+ **What this means.** Running it, and using it on your own data, carries no
348
+ conditions at all. Distributing it, or a modified version of it, means passing on
349
+ the same freedoms — including the source. This is a finished tool rather than a
350
+ library to build on; the libraries it is built from are LGPL, so they can be
351
+ linked from programs under any licence.
281
352
 
282
353
  ## Links
283
354
 
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP ABAP ADT Proxy — management mode.
5
+ *
6
+ * Speaks MCP over stdio and exposes tools that start and stop proxies. This is
7
+ * the command a client registers when it wants to MANAGE proxies; a client that
8
+ * wants to BE proxied still points at `mcp-abap-adt-proxy`.
9
+ *
10
+ * Usage:
11
+ * mcp-abap-adt-proxy-mcp [--config <file>] [--unsafe]
12
+ */
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ function parseArgs() {
18
+ const args = process.argv.slice(2);
19
+ return {
20
+ help: args.includes('--help') || args.includes('-h'),
21
+ version: args.includes('--version') || args.includes('-v'),
22
+ };
23
+ }
24
+
25
+ function showHelp() {
26
+ const pkg = require('../package.json');
27
+ console.log(`
28
+ MCP ABAP ADT Proxy — management mode v${pkg.version}
29
+
30
+ Speaks MCP over stdio. Its tools start and stop authenticating proxies; it does
31
+ not proxy anything itself.
32
+
33
+ Usage:
34
+ mcp-abap-adt-proxy-mcp [options]
35
+
36
+ Tools offered to the client:
37
+ proxy_configs List the proxy configs on this machine, by the name
38
+ proxy_start takes. Call this first — the names cannot be
39
+ guessed.
40
+ proxy_start Start a proxy from one of those configs. The config supplies
41
+ the destination, target URL, default headers and timeouts;
42
+ the PORT does not come from it — a free one is bound, so
43
+ several proxies can run at once, and the URL returned is the
44
+ one actually bound.
45
+ proxy_stop Stop a proxy this session started, freeing its port and
46
+ releasing its credential. Proxies belonging to other sessions
47
+ are never touched.
48
+ proxy_status List this session's proxies and any others on this machine.
49
+ Records whose process has died are pruned when read.
50
+
51
+ Where the configs live:
52
+ Unix: ~/.config/mcp-abap-adt/proxy/<name>.yaml
53
+ Windows: %USERPROFILE%\\Documents\\mcp-abap-adt\\proxy\\<name>.yaml
54
+
55
+ The same files 'mcp-abap-adt-proxy --config <file>' takes. Credentials stay
56
+ in them, resolved through \${VAR} interpolation, and are never passed
57
+ through a tool call.
58
+
59
+ Options:
60
+ --config=<file>, -c Load configuration from a YAML or JSON file
61
+ --env-file=<path> Load a .env file for \${VAR} interpolation
62
+ --unsafe Persist tokens to disk
63
+ --help, -h Show this help message
64
+ --version, -v Show version number
65
+
66
+ Shutting down:
67
+ Every proxy runs in THIS process. Closing the session — or SIGINT, or SIGTERM
68
+ — stops all of them and frees their ports. A proxy nobody has used for 30
69
+ minutes stops itself; that is a backstop, not a substitute for proxy_stop.
70
+
71
+ Registering it with a client:
72
+ {
73
+ "mcpServers": {
74
+ "abap-proxy": { "command": "mcp-abap-adt-proxy-mcp" }
75
+ }
76
+ }
77
+
78
+ For more information, see: https://github.com/fr0ster/mcp-abap-adt-proxy
79
+ `);
80
+ }
81
+
82
+ function main() {
83
+ const args = parseArgs();
84
+
85
+ if (args.help) {
86
+ showHelp();
87
+ process.exit(0);
88
+ }
89
+
90
+ if (args.version) {
91
+ console.log(require('../package.json').version);
92
+ process.exit(0);
93
+ }
94
+
95
+ const serverPath = path.resolve(__dirname, '../dist/mcp/cli.js');
96
+
97
+ if (!fs.existsSync(serverPath)) {
98
+ process.stderr.write(`[MCP Proxy] ✗ Server not found at: ${serverPath}\n`);
99
+ process.stderr.write(
100
+ `[MCP Proxy] Make sure to build the project with 'npm run build' first.\n`,
101
+ );
102
+ process.exit(1);
103
+ return;
104
+ }
105
+
106
+ // Loaded in THIS process, never spawned — the same rule the proxy launcher
107
+ // follows, and it matters more here: the proxies the tools start are
108
+ // listeners in this process, and a spawned child would be orphaned by any
109
+ // signal this launcher did not forward, still holding their ports.
110
+ require(serverPath);
111
+ }
112
+
113
+ main();
package/dist/index.d.ts CHANGED
@@ -17,6 +17,16 @@ export declare class McpAbapAdtProxyServer {
17
17
  private config;
18
18
  private httpServer?;
19
19
  private btpProxy?;
20
+ private builtProxyHandler?;
21
+ /**
22
+ * The request path, shared with the MCP mode. Here an authentication failure
23
+ * is fatal: the process exits so whatever started it can start it again with
24
+ * a credential that works.
25
+ *
26
+ * Built on first use rather than as a field initializer, because `config` is
27
+ * assigned in the constructor and a field initializer runs before that.
28
+ */
29
+ private get proxyHandler();
20
30
  private authExiting;
21
31
  constructor(transportConfig?: TransportConfig, configPath?: string);
22
32
  /**
@@ -31,10 +41,6 @@ export declare class McpAbapAdtProxyServer {
31
41
  * Start HTTP server with request interception
32
42
  */
33
43
  private startHttpServer;
34
- /**
35
- * Handle proxy request - get JWT token and forward transparently via reverse proxy
36
- */
37
- private handleProxyRequest;
38
44
  /**
39
45
  * Start SSE server
40
46
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwBH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAgElC;;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;IAC5B,OAAO,CAAC,WAAW,CAAS;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;IA2E1B;;OAEG;YACW,eAAe;IAgG7B;;OAEG;YACW,kBAAkB;IAgEhC;;OAEG;YACW,cAAc;IA6T5B;;;;;OAKG;YACW,gBAAgB;IAiC9B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBhC;AAGD,eAAe,qBAAqB,CAAC"}
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;AAgElC;;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;IAE5B,OAAO,CAAC,iBAAiB,CAAC,CAA+C;IAEzE;;;;;;;OAOG;IACH,OAAO,KAAK,YAAY,GAevB;IACD,OAAO,CAAC,WAAW,CAAS;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;IA2E1B;;OAEG;YACW,eAAe;IAiE7B;;OAEG;YACW,cAAc;IAiV5B;;;;;OAKG;YACW,gBAAgB;IAiC9B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBhC;AAGD,eAAe,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -11,10 +11,11 @@
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.McpAbapAdtProxyServer = void 0;
13
13
  const node_http_1 = require("node:http");
14
- const interfaces_1 = require("@mcp-abap-adt/interfaces");
14
+ const interfaces_network_1 = require("@mcp-abap-adt/interfaces-network");
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 requestHandler_js_1 = require("./proxy/requestHandler.js");
18
19
  const reverseProxy_js_1 = require("./proxy/reverseProxy.js");
19
20
  const config_js_1 = require("./lib/config.js");
20
21
  const logger_js_1 = require("./lib/logger.js");
@@ -73,6 +74,30 @@ class McpAbapAdtProxyServer {
73
74
  config;
74
75
  httpServer;
75
76
  btpProxy;
77
+ builtProxyHandler;
78
+ /**
79
+ * The request path, shared with the MCP mode. Here an authentication failure
80
+ * is fatal: the process exits so whatever started it can start it again with
81
+ * a credential that works.
82
+ *
83
+ * Built on first use rather than as a field initializer, because `config` is
84
+ * assigned in the constructor and a field initializer runs before that.
85
+ */
86
+ get proxyHandler() {
87
+ if (!this.builtProxyHandler) {
88
+ this.builtProxyHandler = (0, requestHandler_js_1.createProxyRequestHandler)({
89
+ config: this.config,
90
+ proxy: async () => {
91
+ if (!this.btpProxy) {
92
+ this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
93
+ }
94
+ return this.btpProxy;
95
+ },
96
+ onAuthFailure: (error, destination) => this.fatalAuthFailure(error, destination, 'request'),
97
+ });
98
+ }
99
+ return this.builtProxyHandler;
100
+ }
76
101
  authExiting = false;
77
102
  constructor(transportConfig, configPath) {
78
103
  // Load config first to extract transport overrides from YAML/JSON file
@@ -203,39 +228,13 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
203
228
  headers: sanitizedHeaders,
204
229
  remoteAddress: req.socket.remoteAddress,
205
230
  });
206
- // Intercept and analyze request (headers only, body is piped through)
207
- const configOverrides = {
208
- btpDestination: this.config.btpDestination,
209
- targetUrl: this.config.targetUrl,
210
- };
211
- const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, undefined, configOverrides, {
212
- skipHeaderValidation: true,
213
- });
214
- // Check routing decision
215
- if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
216
- logger_js_1.logger?.error('Routing decision failed', {
217
- type: 'ROUTING_DECISION_FAILED',
218
- reason: intercepted.routingDecision.reason,
219
- });
220
- res.writeHead(400, { 'Content-Type': 'application/json' });
221
- res.end(JSON.stringify({
222
- error: intercepted.routingDecision.reason,
223
- }));
224
- return;
225
- }
226
- // Forward request via reverse proxy
227
- logger_js_1.logger?.info('=== PROXYING REQUEST ===', {
228
- type: 'PROXY_REQUEST_START',
229
- btpDestination: intercepted.routingDecision.btpDestination,
230
- });
231
231
  try {
232
- await this.handleProxyRequest(intercepted, req, res);
232
+ await this.proxyHandler(req, res);
233
233
  }
234
234
  catch (error) {
235
235
  logger_js_1.logger?.error('Failed to process request', {
236
236
  type: 'REQUEST_PROCESS_ERROR',
237
237
  error: error instanceof Error ? error.message : String(error),
238
- strategy: intercepted.routingDecision.strategy,
239
238
  });
240
239
  if (!res.headersSent) {
241
240
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -270,62 +269,6 @@ Authorization source: ${authSourceObj}`;
270
269
  });
271
270
  });
272
271
  }
273
- /**
274
- * Handle proxy request - get JWT token and forward transparently via reverse proxy
275
- */
276
- async handleProxyRequest(intercepted, req, res) {
277
- const destination = intercepted.routingDecision.btpDestination;
278
- if (!destination) {
279
- res.writeHead(400, { 'Content-Type': 'application/json' });
280
- res.end(JSON.stringify({ error: 'No BTP destination specified' }));
281
- return;
282
- }
283
- // Ensure proxy is initialized
284
- if (!this.btpProxy) {
285
- this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
286
- }
287
- // Authentication is separated from forwarding: an auth failure is fatal
288
- // (the proxy exits so it can be restarted), while a downstream/forward error
289
- // is returned to the client without killing the proxy.
290
- let jwtToken;
291
- try {
292
- // Get JWT token (cached, auto-refresh)
293
- jwtToken = await this.btpProxy.getJwtToken(destination);
294
- }
295
- catch (authError) {
296
- logger_js_1.logger?.error('Proxy request failed: authentication error', {
297
- type: 'PROXY_REQUEST_AUTH_ERROR',
298
- destination,
299
- error: authError instanceof Error ? authError.message : String(authError),
300
- });
301
- if (!res.headersSent) {
302
- res.writeHead(502, { 'Content-Type': 'application/json' });
303
- res.end(JSON.stringify({ error: 'Authentication failed' }));
304
- }
305
- await this.fatalAuthFailure(authError, destination, 'request');
306
- return;
307
- }
308
- try {
309
- // Get target URL
310
- const targetUrl = intercepted.routingDecision.targetUrl ||
311
- (await this.btpProxy.getTargetUrl(destination));
312
- // Forward request transparently (inject default headers from config)
313
- await (0, reverseProxy_js_1.forwardRequest)(req, res, targetUrl, jwtToken, this.config.defaultHeaders);
314
- }
315
- catch (error) {
316
- logger_js_1.logger?.error('Proxy request failed', {
317
- type: 'PROXY_REQUEST_ERROR',
318
- destination,
319
- error: error instanceof Error ? error.message : String(error),
320
- });
321
- if (!res.headersSent) {
322
- res.writeHead(502, { 'Content-Type': 'application/json' });
323
- res.end(JSON.stringify({
324
- error: error instanceof Error ? error.message : 'Proxy error',
325
- }));
326
- }
327
- }
328
- }
329
272
  /**
330
273
  * Start SSE server
331
274
  */
@@ -371,8 +314,8 @@ Authorization source: ${authSourceObj}`;
371
314
  }
372
315
  }
373
316
  // Add config overrides if not present in headers
374
- if (this.config.btpDestination && !headers[interfaces_1.HEADER_BTP_DESTINATION]) {
375
- headers[interfaces_1.HEADER_BTP_DESTINATION] = this.config.btpDestination;
317
+ if (this.config.btpDestination && !headers[interfaces_network_1.HEADER_BTP_DESTINATION]) {
318
+ headers[interfaces_network_1.HEADER_BTP_DESTINATION] = this.config.btpDestination;
376
319
  }
377
320
  logger_js_1.logger?.debug('SSE request received', {
378
321
  type: 'SSE_HTTP_REQUEST',
@@ -483,14 +426,15 @@ Authorization source: ${authSourceObj}`;
483
426
  }
484
427
  // Read request body
485
428
  let body;
429
+ let rawBody;
486
430
  try {
487
431
  const chunks = [];
488
432
  for await (const chunk of req) {
489
433
  chunks.push(chunk);
490
434
  }
491
435
  if (chunks.length > 0) {
492
- const bodyString = Buffer.concat(chunks).toString('utf-8');
493
- body = JSON.parse(bodyString);
436
+ rawBody = Buffer.concat(chunks);
437
+ body = JSON.parse(rawBody.toString('utf-8'));
494
438
  }
495
439
  }
496
440
  catch (error) {
@@ -532,19 +476,32 @@ Authorization source: ${authSourceObj}`;
532
476
  }));
533
477
  return;
534
478
  }
535
- // Handle proxy request via BtpProxy (JSON-RPC mode for SSE)
479
+ // Forward through the same transparent pipe the other path uses.
480
+ //
481
+ // This used to rebuild the request by hand and carry it over axios,
482
+ // which buffered the response and rewrapped it as a JSON-RPC envelope.
483
+ // The body above was already read — the JSON-RPC `id` in it is what the
484
+ // error envelopes below have to echo — so it is handed over rather than
485
+ // piped. The response still streams, which is the direction that
486
+ // carries an event stream.
536
487
  try {
537
488
  if (!this.btpProxy) {
538
489
  this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
539
490
  }
540
- const proxyResponse = await this.btpProxy.proxyRequest({
541
- method: intercepted.method,
542
- url: intercepted.url,
543
- data: body,
544
- id: getBodyId(body),
545
- }, intercepted.routingDecision, intercepted.headers);
546
- res.writeHead(200, { 'Content-Type': 'application/json' });
547
- res.end(JSON.stringify(proxyResponse));
491
+ const destination = intercepted.routingDecision.btpDestination ??
492
+ this.config.btpDestination;
493
+ if (!destination) {
494
+ throw new Error('No BTP destination for this request');
495
+ }
496
+ const authorization = await this.btpProxy.getAuthorizationHeader(destination);
497
+ const targetUrl = intercepted.routingDecision.targetUrl ||
498
+ (await this.btpProxy.getTargetUrl(destination));
499
+ await (0, reverseProxy_js_1.forwardRequest)(req, res, targetUrl, authorization, this.config.defaultHeaders,
500
+ // The bytes as they arrived, not the parse re-serialised: the
501
+ // client's `content-length` is forwarded unchanged, and a
502
+ // re-serialised body differs from it by whatever whitespace the
503
+ // sender used.
504
+ rawBody);
548
505
  }
549
506
  catch (error) {
550
507
  logger_js_1.logger?.error('Failed to process SSE POST request', {
@@ -14,10 +14,31 @@ import type { IServiceKeyStore, ISessionStore } from '@mcp-abap-adt/auth-broker'
14
14
  * - Windows: %USERPROFILE%\Documents\mcp-abap-adt\{subfolder}
15
15
  * 3. Current working directory (process.cwd())
16
16
  *
17
- * @param subfolder Subfolder name ('service-keys' or 'sessions')
17
+ * @param subfolder One of the four folders; see {@link StoreFolder}
18
18
  * @returns Array of resolved absolute paths
19
19
  */
20
- export declare function getPlatformPaths(subfolder?: 'service-keys' | 'sessions'): string[];
20
+ /**
21
+ * The four folders the toolchain keeps under one base.
22
+ *
23
+ * `service-keys/` and `sessions/` are the auth broker's. `proxy/` holds one
24
+ * ready config per proxy — the files `--config` takes. `runtime/` holds a
25
+ * record per live proxy, so one session can see another's.
26
+ */
27
+ export declare const STORE_FOLDERS: readonly ["service-keys", "sessions", "proxy", "runtime"];
28
+ export type StoreFolder = (typeof STORE_FOLDERS)[number];
29
+ /**
30
+ * THE base directory — one answer, not a search path.
31
+ *
32
+ * `AUTH_BROKER_PATH` relocates it, so all four folders move together; when it
33
+ * lists several, the first is the one written to. Unlike
34
+ * {@link getPlatformPaths}, this never falls back to the working directory: a
35
+ * file written beside wherever a client happened to be launched from is a file
36
+ * the next session will not find.
37
+ */
38
+ export declare function storeBaseDir(): string;
39
+ /** THE directory for one of the four folders. */
40
+ export declare function storeDir(folder: StoreFolder): string;
41
+ export declare function getPlatformPaths(subfolder?: StoreFolder): string[];
21
42
  /**
22
43
  * Get platform-specific stores
23
44
  * Returns XSUAA stores for BTP authentication:
@@ -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;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"}
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;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,2DAKhB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,IAAI,MAAM,CA0BrC;AAED,iDAAiD;AACjD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE,CAgElE;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"}
@@ -38,6 +38,9 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.STORE_FOLDERS = void 0;
42
+ exports.storeBaseDir = storeBaseDir;
43
+ exports.storeDir = storeDir;
41
44
  exports.getPlatformPaths = getPlatformPaths;
42
45
  exports.getPlatformStores = getPlatformStores;
43
46
  const os = __importStar(require("node:os"));
@@ -53,9 +56,61 @@ const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
53
56
  * - Windows: %USERPROFILE%\Documents\mcp-abap-adt\{subfolder}
54
57
  * 3. Current working directory (process.cwd())
55
58
  *
56
- * @param subfolder Subfolder name ('service-keys' or 'sessions')
59
+ * @param subfolder One of the four folders; see {@link StoreFolder}
57
60
  * @returns Array of resolved absolute paths
58
61
  */
62
+ /**
63
+ * The four folders the toolchain keeps under one base.
64
+ *
65
+ * `service-keys/` and `sessions/` are the auth broker's. `proxy/` holds one
66
+ * ready config per proxy — the files `--config` takes. `runtime/` holds a
67
+ * record per live proxy, so one session can see another's.
68
+ */
69
+ exports.STORE_FOLDERS = [
70
+ 'service-keys',
71
+ 'sessions',
72
+ 'proxy',
73
+ 'runtime',
74
+ ];
75
+ /**
76
+ * THE base directory — one answer, not a search path.
77
+ *
78
+ * `AUTH_BROKER_PATH` relocates it, so all four folders move together; when it
79
+ * lists several, the first is the one written to. Unlike
80
+ * {@link getPlatformPaths}, this never falls back to the working directory: a
81
+ * file written beside wherever a client happened to be launched from is a file
82
+ * the next session will not find.
83
+ */
84
+ function storeBaseDir() {
85
+ const envPath = process.env.AUTH_BROKER_PATH;
86
+ if (envPath) {
87
+ // Not `/[:;]/`: on Windows that splits `C:\store` into `C`, which then
88
+ // resolves against the working directory. The delimiter is chosen from
89
+ // `process.platform` rather than taken from `path.delimiter`, which is bound
90
+ // to the host at module load and so cannot be exercised for the other one.
91
+ const first = envPath
92
+ .split(process.platform === 'win32' ? ';' : ':')
93
+ .map((p) => p.trim())
94
+ .find((p) => p.length > 0);
95
+ if (first) {
96
+ const resolved = path.resolve(first);
97
+ // Pointed at one of the folders rather than the base — a plausible
98
+ // mistake that `getPlatformPaths` already tolerates, so this must too.
99
+ // Otherwise runtime records land in `.../service-keys/runtime`.
100
+ return exports.STORE_FOLDERS.includes(path.basename(resolved))
101
+ ? path.dirname(resolved)
102
+ : resolved;
103
+ }
104
+ }
105
+ const homeDir = os.homedir();
106
+ return process.platform === 'win32'
107
+ ? path.join(homeDir, 'Documents', 'mcp-abap-adt')
108
+ : path.join(homeDir, '.config', 'mcp-abap-adt');
109
+ }
110
+ /** THE directory for one of the four folders. */
111
+ function storeDir(folder) {
112
+ return path.join(storeBaseDir(), folder);
113
+ }
59
114
  function getPlatformPaths(subfolder) {
60
115
  const paths = [];
61
116
  const isWindows = process.platform === 'win32';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/mcp/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // src/mcp/cli.ts
4
+ const config_js_1 = require("../lib/config.js");
5
+ const logger_js_1 = require("../lib/logger.js");
6
+ const server_js_1 = require("./server.js");
7
+ /**
8
+ * What `bin/mcp-abap-adt-proxy-mcp.js` loads.
9
+ *
10
+ * Nothing but wiring: the bin parses `--help`/`--version` and then requires
11
+ * this, in the same process, so the listeners the tools start die with the
12
+ * session that asked for them.
13
+ */
14
+ (0, server_js_1.runMcpMode)((0, config_js_1.loadConfig)()).catch((error) => {
15
+ logger_js_1.logger?.error('MCP mode failed to start', {
16
+ type: 'MCP_MODE_START_FAILED',
17
+ error: error instanceof Error ? error.message : String(error),
18
+ });
19
+ process.stderr.write(`[MCP Proxy] ✗ ${error instanceof Error ? error.message : String(error)}\n`);
20
+ process.exit(1);
21
+ });