@mcp-abap-adt/proxy 1.0.0 → 1.0.2
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 +16 -0
- package/README.md +22 -0
- package/bin/mcp-abap-adt-proxy.js +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +32 -0
- package/dist/proxy/reverseProxy.d.ts +1 -1
- package/dist/proxy/reverseProxy.d.ts.map +1 -1
- package/dist/proxy/reverseProxy.js +17 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.2] - 2026-04-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Skip header validation in SSE transport path** — proxy no longer runs `validateProxyHeaders()` on SSE requests, matching HTTP transport behavior. Proxy should not validate client headers beyond authorization and defaults.
|
|
14
|
+
- **Added debug-level header forwarding log** in reverse proxy for diagnostics (`LOG_LEVEL=debug`)
|
|
15
|
+
|
|
16
|
+
## [1.0.1] - 2026-04-07
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Default headers injection (`defaultHeaders` / `--header`)**
|
|
20
|
+
- Proxy can inject configurable headers into every forwarded request
|
|
21
|
+
- YAML config: `defaultHeaders` map (e.g., `x-sap-destination: S4HANA_E19`)
|
|
22
|
+
- CLI: repeatable `--header key=value` flag
|
|
23
|
+
- Client headers override defaults (low priority injection)
|
|
24
|
+
- Enables MCP clients (Cline, Claude Code) that cannot set custom headers to pass SAP headers to the MCP server on BTP
|
|
25
|
+
|
|
10
26
|
## [1.0.0] - 2026-04-07
|
|
11
27
|
|
|
12
28
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -93,6 +93,28 @@ For detailed setup instructions for Cline and GitHub Copilot, see the **[Client
|
|
|
93
93
|
- `--browser-auth-port=<port>` - Port for OAuth2 callback (default: 3333)
|
|
94
94
|
- `--unsafe` - Enables file-based session storage (persists tokens to disk). By default, sessions are stored in-memory (secure, lost on restart)
|
|
95
95
|
|
|
96
|
+
**Default Headers:**
|
|
97
|
+
|
|
98
|
+
MCP clients like Cline and Claude Code cannot set arbitrary request headers. Use default headers to inject SAP-specific headers (e.g. `x-sap-destination`, `x-sap-client`) that the target MCP server requires.
|
|
99
|
+
|
|
100
|
+
Client-supplied headers always take precedence over defaults.
|
|
101
|
+
|
|
102
|
+
Via YAML config (`defaultHeaders` map):
|
|
103
|
+
```yaml
|
|
104
|
+
btpDestination: mcp
|
|
105
|
+
targetUrl: https://example.com
|
|
106
|
+
defaultHeaders:
|
|
107
|
+
x-sap-destination: S4HANA_E19
|
|
108
|
+
x-sap-client: "100"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Via CLI (`--header`, repeatable):
|
|
112
|
+
```bash
|
|
113
|
+
mcp-abap-adt-proxy --btp=mcp --url=https://example.com \
|
|
114
|
+
--header x-sap-destination=S4HANA_E19 \
|
|
115
|
+
--header x-sap-client=100
|
|
116
|
+
```
|
|
117
|
+
|
|
96
118
|
**How It Works:**
|
|
97
119
|
|
|
98
120
|
The proxy uses BTP/XSUAA authentication:
|
|
@@ -62,6 +62,8 @@ Options:
|
|
|
62
62
|
--config=<file>, -c Load configuration from YAML or JSON file
|
|
63
63
|
Alternative to command-line parameters
|
|
64
64
|
--unsafe Enable file-based session storage (persists tokens to disk)
|
|
65
|
+
--header key=value Add default header to every forwarded request (repeatable)
|
|
66
|
+
Example: --header x-sap-destination=S4HANA
|
|
65
67
|
--help, -h Show this help message
|
|
66
68
|
--version, -v Show version number
|
|
67
69
|
|
|
@@ -82,6 +84,7 @@ Examples:
|
|
|
82
84
|
mcp-abap-adt-proxy --transport=sse --sse-port=3002 # SSE mode on port 3002
|
|
83
85
|
mcp-abap-adt-proxy --btp=ai # With BTP destination override
|
|
84
86
|
mcp-abap-adt-proxy --btp=ai --unsafe # With file-based session storage
|
|
87
|
+
mcp-abap-adt-proxy --btp=ai --header x-sap-destination=S4HANA # With default SAP headers
|
|
85
88
|
mcp-abap-adt-proxy tui # Interactive configuration wizard
|
|
86
89
|
mcp-abap-adt-proxy --config=proxy-config.yaml # Load configuration from YAML file
|
|
87
90
|
mcp-abap-adt-proxy -c proxy-config.yml # Load configuration from YAML file (short form)
|
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;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;
|
|
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;IA6T5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAmBhC;AAGD,eAAe,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -252,8 +252,8 @@ Authorization source: ${authSourceObj}`;
|
|
|
252
252
|
// Get target URL
|
|
253
253
|
const targetUrl = intercepted.routingDecision.targetUrl ||
|
|
254
254
|
(await this.btpProxy.getTargetUrl(destination));
|
|
255
|
-
// Forward request transparently
|
|
256
|
-
await (0, reverseProxy_js_1.forwardRequest)(req, res, targetUrl, jwtToken);
|
|
255
|
+
// Forward request transparently (inject default headers from config)
|
|
256
|
+
await (0, reverseProxy_js_1.forwardRequest)(req, res, targetUrl, jwtToken, this.config.defaultHeaders);
|
|
257
257
|
}
|
|
258
258
|
catch (error) {
|
|
259
259
|
logger_js_1.logger?.error('Proxy request failed', {
|
|
@@ -456,7 +456,9 @@ Authorization source: ${authSourceObj}`;
|
|
|
456
456
|
btpDestination: this.config.btpDestination,
|
|
457
457
|
targetUrl: this.config.targetUrl,
|
|
458
458
|
};
|
|
459
|
-
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides
|
|
459
|
+
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides, {
|
|
460
|
+
skipHeaderValidation: true,
|
|
461
|
+
});
|
|
460
462
|
// Check routing decision
|
|
461
463
|
if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
|
|
462
464
|
logger_js_1.logger?.error('Routing decision failed for SSE POST', {
|
package/dist/lib/config.d.ts
CHANGED
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;
|
|
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;IAEnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAGxC,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;AA4ID;;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
|
@@ -137,6 +137,7 @@ function applyDefaults(fileConfig) {
|
|
|
137
137
|
logLevel: fileConfig.logLevel || 'info',
|
|
138
138
|
btpDestination: fileConfig.btpDestination,
|
|
139
139
|
targetUrl: fileConfig.targetUrl,
|
|
140
|
+
defaultHeaders: fileConfig.defaultHeaders,
|
|
140
141
|
unsafe: fileConfig.unsafe ?? false,
|
|
141
142
|
maxRetries: fileConfig.maxRetries ?? 3,
|
|
142
143
|
retryDelay: fileConfig.retryDelay ?? 1000,
|
|
@@ -161,6 +162,20 @@ function loadFromEnv() {
|
|
|
161
162
|
const browserAuthPort = browserAuthPortStr
|
|
162
163
|
? parseInt(browserAuthPortStr, 10)
|
|
163
164
|
: undefined;
|
|
165
|
+
// Parse repeatable --header key=value arguments
|
|
166
|
+
const headerArgs = getAllArgValues('--header');
|
|
167
|
+
let defaultHeaders;
|
|
168
|
+
if (headerArgs.length > 0) {
|
|
169
|
+
defaultHeaders = {};
|
|
170
|
+
for (const arg of headerArgs) {
|
|
171
|
+
const eqIndex = arg.indexOf('=');
|
|
172
|
+
if (eqIndex > 0) {
|
|
173
|
+
const key = arg.substring(0, eqIndex).toLowerCase();
|
|
174
|
+
const value = arg.substring(eqIndex + 1);
|
|
175
|
+
defaultHeaders[key] = value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
164
179
|
return {
|
|
165
180
|
httpPort: parseInt(process.env.MCP_HTTP_PORT || '3001', 10),
|
|
166
181
|
ssePort: parseInt(process.env.MCP_SSE_PORT || '3002', 10),
|
|
@@ -169,6 +184,7 @@ function loadFromEnv() {
|
|
|
169
184
|
logLevel: process.env.LOG_LEVEL || 'info',
|
|
170
185
|
btpDestination,
|
|
171
186
|
targetUrl,
|
|
187
|
+
defaultHeaders,
|
|
172
188
|
unsafe,
|
|
173
189
|
maxRetries: parseInt(process.env.MCP_PROXY_MAX_RETRIES || '3', 10),
|
|
174
190
|
retryDelay: parseInt(process.env.MCP_PROXY_RETRY_DELAY || '1000', 10),
|
|
@@ -194,6 +210,22 @@ function getArgValue(argName) {
|
|
|
194
210
|
}
|
|
195
211
|
return undefined;
|
|
196
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Get all values for a repeatable argument from command line
|
|
215
|
+
*/
|
|
216
|
+
function getAllArgValues(argName) {
|
|
217
|
+
const args = process.argv;
|
|
218
|
+
const values = [];
|
|
219
|
+
for (let i = 0; i < args.length; i++) {
|
|
220
|
+
if (args[i] === argName && i + 1 < args.length) {
|
|
221
|
+
values.push(args[i + 1]);
|
|
222
|
+
}
|
|
223
|
+
if (args[i].startsWith(`${argName}=`)) {
|
|
224
|
+
values.push(args[i].substring(argName.length + 1));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return values;
|
|
228
|
+
}
|
|
197
229
|
/**
|
|
198
230
|
* Get config file path from command line (--config or -c)
|
|
199
231
|
*/
|
|
@@ -3,5 +3,5 @@ import * as http from 'node:http';
|
|
|
3
3
|
* Forward an HTTP request to a target URL with JWT injection.
|
|
4
4
|
* Streams both request and response using pipe().
|
|
5
5
|
*/
|
|
6
|
-
export declare function forwardRequest(clientReq: http.IncomingMessage, clientRes: http.ServerResponse, targetBaseUrl: string, jwtToken: string): Promise<void>;
|
|
6
|
+
export declare function forwardRequest(clientReq: http.IncomingMessage, clientRes: http.ServerResponse, targetBaseUrl: string, jwtToken: string, defaultHeaders?: Record<string, string>): Promise<void>;
|
|
7
7
|
//# sourceMappingURL=reverseProxy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reverseProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/reverseProxy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAoBlC;;;GAGG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,IAAI,CAAC,eAAe,EAC/B,SAAS,EAAE,IAAI,CAAC,cAAc,EAC9B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"reverseProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/reverseProxy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAoBlC;;;GAGG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,IAAI,CAAC,eAAe,EAC/B,SAAS,EAAE,IAAI,CAAC,cAAc,EAC9B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAiGf"}
|
|
@@ -57,10 +57,17 @@ const HOP_BY_HOP_HEADERS = new Set([
|
|
|
57
57
|
* Forward an HTTP request to a target URL with JWT injection.
|
|
58
58
|
* Streams both request and response using pipe().
|
|
59
59
|
*/
|
|
60
|
-
async function forwardRequest(clientReq, clientRes, targetBaseUrl, jwtToken) {
|
|
60
|
+
async function forwardRequest(clientReq, clientRes, targetBaseUrl, jwtToken, defaultHeaders) {
|
|
61
61
|
const targetUrl = new node_url_1.URL(clientReq.url || '/', targetBaseUrl);
|
|
62
|
-
// Build forwarded headers
|
|
62
|
+
// Build forwarded headers: defaults first, then client headers override
|
|
63
63
|
const forwardedHeaders = {};
|
|
64
|
+
// 1. Inject default headers (low priority)
|
|
65
|
+
if (defaultHeaders) {
|
|
66
|
+
for (const [key, value] of Object.entries(defaultHeaders)) {
|
|
67
|
+
forwardedHeaders[key.toLowerCase()] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// 2. Copy client headers (high priority — overrides defaults)
|
|
64
71
|
for (const [key, value] of Object.entries(clientReq.headers)) {
|
|
65
72
|
if (HOP_BY_HOP_HEADERS.has(key.toLowerCase()))
|
|
66
73
|
continue;
|
|
@@ -76,6 +83,14 @@ async function forwardRequest(clientReq, clientRes, targetBaseUrl, jwtToken) {
|
|
|
76
83
|
}
|
|
77
84
|
// Set correct host for target
|
|
78
85
|
forwardedHeaders.host = targetUrl.host;
|
|
86
|
+
// Debug: log incoming vs forwarded headers to diagnose header drops
|
|
87
|
+
logger_js_1.logger?.debug('Headers forwarding detail', {
|
|
88
|
+
type: 'REVERSE_PROXY_HEADERS',
|
|
89
|
+
incoming: Object.keys(clientReq.headers),
|
|
90
|
+
forwarded: Object.keys(forwardedHeaders),
|
|
91
|
+
hasAccept: !!forwardedHeaders.accept || !!forwardedHeaders.Accept,
|
|
92
|
+
defaultHeaderKeys: defaultHeaders ? Object.keys(defaultHeaders) : [],
|
|
93
|
+
});
|
|
79
94
|
const isHttps = targetUrl.protocol === 'https:';
|
|
80
95
|
const transport = isHttps ? https : http;
|
|
81
96
|
const options = {
|
package/package.json
CHANGED