@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.
- package/CHANGELOG.md +217 -0
- package/LICENSE +669 -17
- package/README.md +79 -8
- package/bin/mcp-abap-adt-proxy-mcp.js +113 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -97
- package/dist/lib/stores.d.ts +23 -2
- package/dist/lib/stores.d.ts.map +1 -1
- package/dist/lib/stores.js +56 -1
- package/dist/mcp/cli.d.ts +2 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +21 -0
- package/dist/mcp/configs.d.ts +33 -0
- package/dist/mcp/configs.d.ts.map +1 -0
- package/dist/mcp/configs.js +142 -0
- package/dist/mcp/ports.d.ts +15 -0
- package/dist/mcp/ports.d.ts.map +1 -0
- package/dist/mcp/ports.js +35 -0
- package/dist/mcp/registry.d.ts +57 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +145 -0
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +79 -0
- package/dist/mcp/shutdown.d.ts +37 -0
- package/dist/mcp/shutdown.d.ts.map +1 -0
- package/dist/mcp/shutdown.js +82 -0
- package/dist/mcp/supervisor.d.ts +125 -0
- package/dist/mcp/supervisor.d.ts.map +1 -0
- package/dist/mcp/supervisor.js +330 -0
- package/dist/mcp/tools.d.ts +28 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +152 -0
- package/dist/proxy/btpProxy.d.ts +24 -73
- package/dist/proxy/btpProxy.d.ts.map +1 -1
- package/dist/proxy/btpProxy.js +116 -631
- package/dist/proxy/credentials.d.ts +45 -0
- package/dist/proxy/credentials.d.ts.map +1 -0
- package/dist/proxy/credentials.js +41 -0
- package/dist/proxy/requestHandler.d.ts +38 -0
- package/dist/proxy/requestHandler.d.ts.map +1 -0
- package/dist/proxy/requestHandler.js +73 -0
- package/dist/proxy/reverseProxy.d.ts +11 -2
- package/dist/proxy/reverseProxy.d.ts.map +1 -1
- package/dist/proxy/reverseProxy.js +52 -9
- package/dist/router/headerAnalyzer.js +2 -2
- package/dist/router/requestInterceptor.js +9 -9
- package/docs/API.md +172 -0
- package/docs/ARCHITECTURE.md +322 -0
- package/docs/CLIENT_SETUP.md +413 -0
- package/docs/CONFIGURATION.md +258 -0
- package/docs/MIGRATION-4.0.md +125 -0
- package/docs/ROUTING_LOGIC.md +126 -0
- package/docs/TROUBLESHOOTING.md +488 -0
- package/docs/USAGE.md +422 -0
- package/docs/YAML_CONFIG.md +273 -0
- package/docs/mcp-proxy-config.example.yaml +62 -0
- package/package.json +17 -10
- package/dist/proxy/cloudLlmHubProxy.d.ts +0 -2
- package/dist/proxy/cloudLlmHubProxy.d.ts.map +0 -1
- 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
|
-
- ✅ **
|
|
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
|
-
- ✅ **
|
|
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** -
|
|
238
|
-
- **
|
|
239
|
-
- **
|
|
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
|
-
|
|
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
|
*/
|
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;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
|
|
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.
|
|
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[
|
|
375
|
-
headers[
|
|
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
|
-
|
|
493
|
-
body = JSON.parse(
|
|
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
|
-
//
|
|
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
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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', {
|
package/dist/lib/stores.d.ts
CHANGED
|
@@ -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
|
|
17
|
+
* @param subfolder One of the four folders; see {@link StoreFolder}
|
|
18
18
|
* @returns Array of resolved absolute paths
|
|
19
19
|
*/
|
|
20
|
-
|
|
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:
|
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;AAOnC;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,
|
|
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"}
|
package/dist/lib/stores.js
CHANGED
|
@@ -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
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/mcp/cli.ts"],"names":[],"mappings":""}
|
package/dist/mcp/cli.js
ADDED
|
@@ -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
|
+
});
|