@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 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)
@@ -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;IA2T5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAmBhC;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;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', {
@@ -9,6 +9,7 @@ export interface ProxyConfig {
9
9
  logLevel: string;
10
10
  btpDestination?: string;
11
11
  targetUrl?: string;
12
+ defaultHeaders?: Record<string, string>;
12
13
  unsafe?: boolean;
13
14
  maxRetries?: number;
14
15
  retryDelay?: number;
@@ -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;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"}
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"}
@@ -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,GACf,OAAO,CAAC,IAAI,CAAC,CA+Ef"}
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 (skip hop-by-hop and replace auth)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/proxy",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MCP proxy server for SAP ABAP ADT - proxies local requests to cloud-llm-hub with JWT authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",