@mcp-abap-adt/proxy 0.1.9 → 1.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 +35 -7
- package/README.md +9 -9
- package/bin/mcp-abap-adt-proxy.js +32 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -276
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +34 -1
- package/dist/lib/stores.d.ts +14 -0
- package/dist/lib/stores.d.ts.map +1 -1
- package/dist/lib/stores.js +1 -0
- package/dist/lib/transportConfig.d.ts +15 -2
- package/dist/lib/transportConfig.d.ts.map +1 -1
- package/dist/lib/transportConfig.js +41 -33
- package/dist/proxy/btpProxy.d.ts +8 -3
- package/dist/proxy/btpProxy.d.ts.map +1 -1
- package/dist/proxy/btpProxy.js +21 -5
- package/dist/proxy/reverseProxy.d.ts +7 -0
- package/dist/proxy/reverseProxy.d.ts.map +1 -0
- package/dist/proxy/reverseProxy.js +130 -0
- package/dist/router/headerAnalyzer.d.ts +3 -3
- package/dist/router/headerAnalyzer.js +7 -7
- package/dist/router/requestInterceptor.d.ts +2 -0
- package/dist/router/requestInterceptor.d.ts.map +1 -1
- package/dist/router/requestInterceptor.js +17 -15
- package/dist/tui/index.d.ts +8 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +11 -0
- package/dist/tui/serviceKeyCheck.d.ts +25 -0
- package/dist/tui/serviceKeyCheck.d.ts.map +1 -0
- package/dist/tui/serviceKeyCheck.js +62 -0
- package/dist/tui/wizard.d.ts +38 -0
- package/dist/tui/wizard.d.ts.map +1 -0
- package/dist/tui/wizard.js +321 -0
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.0] - 2026-04-07
|
|
11
|
+
|
|
12
|
+
### Breaking Changes
|
|
13
|
+
- **Header renamed: `x-btp-destination` → `x-sap-destination`**
|
|
14
|
+
- The proxy now uses `x-sap-destination` header (same as MCP server) instead of the proxy-specific `x-btp-destination`
|
|
15
|
+
- The `--btp` CLI parameter is unchanged
|
|
16
|
+
- Clients sending `x-btp-destination` header must update to `x-sap-destination`
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Transparent reverse proxy for BTP request forwarding**
|
|
20
|
+
- Replaced buffered axios-based proxy with Node.js native HTTP streaming reverse proxy
|
|
21
|
+
- Supports any HTTP API including OpenAI-compatible streaming endpoints
|
|
22
|
+
- `targetUrl` parameter for explicit target URL override (`--target-url`/`--url` or `x-target-url` header)
|
|
23
|
+
- **Interactive configuration wizard (TUI)**
|
|
24
|
+
- `npx @mcp-abap-adt/proxy --tui` launches interactive setup wizard
|
|
25
|
+
- Service key existence checker validates BTP destination before configuration
|
|
26
|
+
- Generates YAML config files with inline comments
|
|
27
|
+
- Supports BTP and Direct URL scenarios with advanced settings
|
|
28
|
+
- **Dependencies updated**
|
|
29
|
+
- `@mcp-abap-adt/interfaces` bumped to v5
|
|
30
|
+
- `@mcp-abap-adt/auth-stores` bumped to v1.0.4
|
|
31
|
+
- Added `@inquirer/prompts` for TUI wizard
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **Header validation skipped in reverse proxy mode** when config provides destination
|
|
35
|
+
- **YAML config file now correctly loads transport/port settings** (`--config` flag)
|
|
36
|
+
- **targetUrl loading from YAML config** now works correctly
|
|
37
|
+
|
|
10
38
|
## [0.1.9] - 2026-02-12
|
|
11
39
|
|
|
12
40
|
### Changed
|
|
@@ -45,7 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
45
73
|
|
|
46
74
|
### Added
|
|
47
75
|
- **Separate AuthBroker instances for BTP and ABAP destinations**
|
|
48
|
-
- `btpAuthBroker` with `XsuaaTokenProvider` for BTP destinations (`--btp` or `x-
|
|
76
|
+
- `btpAuthBroker` with `XsuaaTokenProvider` for BTP destinations (`--btp` or `x-sap-destination`)
|
|
49
77
|
- `abapAuthBroker` with `BtpTokenProvider` for ABAP destinations (`--mcp` or `x-mcp-destination`)
|
|
50
78
|
- Proper separation ensures correct token provider is used for each destination type
|
|
51
79
|
- **Client Setup Guide** (`docs/CLIENT_SETUP.md`)
|
|
@@ -89,7 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
89
117
|
- **Documentation for BTP-Only authentication mode**
|
|
90
118
|
- Added "Scenario 2: BTP Authentication Only (No SAP Configuration)" in `docs/USAGE.md`
|
|
91
119
|
- Documented use case for connecting to any BTP service without SAP ABAP configuration
|
|
92
|
-
- Explained that `x-mcp-destination` is optional when using `x-
|
|
120
|
+
- Explained that `x-mcp-destination` is optional when using `x-sap-destination` or `--btp`
|
|
93
121
|
- Updated README.md with "BTP-Only Mode" section explaining behavior when only `--btp` is provided
|
|
94
122
|
- **Enhanced help text in `bin/mcp-abap-adt-proxy.js`**
|
|
95
123
|
- Added detailed "Usage Modes" section with three modes:
|
|
@@ -111,7 +139,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
111
139
|
### Fixed
|
|
112
140
|
- **Unit tests updated to match new routing logic**
|
|
113
141
|
- Fixed tests that expected `UNKNOWN` strategy when only `x-mcp-destination` or `x-mcp-url` is provided
|
|
114
|
-
- Updated test expectations to reflect that `x-
|
|
142
|
+
- Updated test expectations to reflect that `x-sap-destination` is now optional
|
|
115
143
|
- Added tests for `x-mcp-url` header support
|
|
116
144
|
- Updated `shouldProxy` function tests to cover all routing scenarios (BTP, MCP destination, MCP URL)
|
|
117
145
|
- All 51 unit tests now pass successfully
|
|
@@ -134,7 +162,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
134
162
|
|
|
135
163
|
### Added
|
|
136
164
|
- **Local testing mode without BTP authentication**
|
|
137
|
-
- Proxy can now run with only `--mcp` or `x-mcp-destination` (without `--btp` or `x-
|
|
165
|
+
- Proxy can now run with only `--mcp` or `x-mcp-destination` (without `--btp` or `x-sap-destination`)
|
|
138
166
|
- Enables local integration testing without BTP authentication
|
|
139
167
|
- MCP server URL can be obtained from MCP destination service key or specified directly via `--mcp-url`
|
|
140
168
|
- **`--mcp-url` parameter support**
|
|
@@ -149,16 +177,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
149
177
|
- Default configuration: ADT on port 3000 (HTTP), Proxy on port 3001 (HTTP)
|
|
150
178
|
- Available via `npm run test:servers`
|
|
151
179
|
- **Flexible routing requirements**
|
|
152
|
-
- Proxy accepts either `x-
|
|
180
|
+
- Proxy accepts either `x-sap-destination/--btp`, `x-mcp-destination/--mcp`, or `x-mcp-url/--mcp-url`
|
|
153
181
|
- Removed strict requirement for BTP destination when using MCP destination or URL
|
|
154
182
|
|
|
155
183
|
### Changed
|
|
156
184
|
- **Routing logic updated**
|
|
157
|
-
- `x-
|
|
185
|
+
- `x-sap-destination` is now optional (was required)
|
|
158
186
|
- Proxy can work with only `x-mcp-destination` or `x-mcp-url` for local testing
|
|
159
187
|
- MCP URL can be obtained from multiple sources (priority order):
|
|
160
188
|
1. `x-mcp-url` header or `--mcp-url` parameter (direct URL)
|
|
161
|
-
2. Service key for `x-
|
|
189
|
+
2. Service key for `x-sap-destination` (if provided)
|
|
162
190
|
3. Service key for `x-mcp-destination` (if only MCP destination is provided)
|
|
163
191
|
- **Token retrieval made optional for MCP destination**
|
|
164
192
|
- Token retrieval for `mcpDestination` is now optional (won't fail if token unavailable)
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ For detailed setup instructions for Cline and GitHub Copilot, see the **[Client
|
|
|
76
76
|
"type": "streamableHttp",
|
|
77
77
|
"url": "http://localhost:3001/mcp/stream/http",
|
|
78
78
|
"headers": {
|
|
79
|
-
"x-
|
|
79
|
+
"x-sap-destination": "btp-cloud"
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -84,10 +84,10 @@ For detailed setup instructions for Cline and GitHub Copilot, see the **[Client
|
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
**Required Headers:**
|
|
87
|
-
- `x-
|
|
87
|
+
- `x-sap-destination` - Destination name for BTP Cloud authorization token and MCP server URL
|
|
88
88
|
|
|
89
89
|
**Command Line Overrides:**
|
|
90
|
-
- `--btp=<destination>` - Overrides `x-
|
|
90
|
+
- `--btp=<destination>` - Overrides `x-sap-destination` header (takes precedence)
|
|
91
91
|
- `--url=<url>` - Overrides MCP server URL (required if service key lacks URL)
|
|
92
92
|
- `--browser=<browser>` - Browser to use: `system` (default), `chrome`, `edge`, `firefox`, `headless`
|
|
93
93
|
- `--browser-auth-port=<port>` - Port for OAuth2 callback (default: 3333)
|
|
@@ -97,7 +97,7 @@ For detailed setup instructions for Cline and GitHub Copilot, see the **[Client
|
|
|
97
97
|
|
|
98
98
|
The proxy uses BTP/XSUAA authentication:
|
|
99
99
|
|
|
100
|
-
1. **BTP Authentication** (if `--btp` or `x-
|
|
100
|
+
1. **BTP Authentication** (if `--btp` or `x-sap-destination` is present):
|
|
101
101
|
- Uses `AuthorizationCodeProvider` (browser-based OAuth2 flow)
|
|
102
102
|
- **Eager Authentication**: Opens browser immediately on startup to get token
|
|
103
103
|
- Injects/overwrites `Authorization: Bearer <token>` header
|
|
@@ -106,7 +106,7 @@ The proxy uses BTP/XSUAA authentication:
|
|
|
106
106
|
|
|
107
107
|
**BTP Authentication Mode** (with `--btp`):
|
|
108
108
|
1. Proxy starts → Opens browser for login (Eager Auth) → Gets/Refreshes JWT token
|
|
109
|
-
2. `x-
|
|
109
|
+
2. `x-sap-destination` (or `--btp`) → Adds `Authorization: Bearer <token>` header
|
|
110
110
|
3. MCP server URL obtained from service key OR `--url` parameter
|
|
111
111
|
|
|
112
112
|
## Documentation
|
|
@@ -126,16 +126,16 @@ The proxy uses BTP/XSUAA authentication:
|
|
|
126
126
|
|
|
127
127
|
The proxy performs the following steps for each request:
|
|
128
128
|
|
|
129
|
-
1. **Extract Headers**: Reads `x-
|
|
129
|
+
1. **Extract Headers**: Reads `x-sap-destination` header
|
|
130
130
|
2. **Apply Command Line Overrides**: `--btp` parameter overrides header (if provided)
|
|
131
|
-
3. **Validate Routing Requirements**: Requires `x-
|
|
132
|
-
4. **BTP Authentication** (if `x-
|
|
131
|
+
3. **Validate Routing Requirements**: Requires `x-sap-destination/--btp`
|
|
132
|
+
4. **BTP Authentication** (if `x-sap-destination` or `--btp` is provided):
|
|
133
133
|
- Uses `AuthorizationCodeProvider` (browser-based login)
|
|
134
134
|
- **Eagerly** obtains token on startup (if configured via `--btp`)
|
|
135
135
|
- Retrieves JWT token using cached refresh token or opens browser
|
|
136
136
|
- Injects/overwrites `Authorization: Bearer <token>` header
|
|
137
137
|
5. **Get MCP Server URL**:
|
|
138
|
-
- From service key for `x-
|
|
138
|
+
- From service key for `x-sap-destination`
|
|
139
139
|
6. **Forward Request**: Sends request to MCP server URL with all injected headers
|
|
140
140
|
7. **Return Response**: Forwards the response back to the client
|
|
141
141
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* mcp-abap-adt-proxy [options]
|
|
10
|
+
* mcp-abap-adt-proxy tui
|
|
10
11
|
* mcp-abap-adt-proxy --transport=stdio
|
|
11
12
|
* mcp-abap-adt-proxy --transport=http
|
|
12
13
|
*/
|
|
@@ -43,6 +44,10 @@ MCP ABAP ADT Proxy Server v${pkg.version}
|
|
|
43
44
|
|
|
44
45
|
Usage:
|
|
45
46
|
mcp-abap-adt-proxy [options]
|
|
47
|
+
mcp-abap-adt-proxy tui
|
|
48
|
+
|
|
49
|
+
Commands:
|
|
50
|
+
tui Interactive configuration wizard (generates YAML config)
|
|
46
51
|
|
|
47
52
|
Options:
|
|
48
53
|
--transport=<type> Transport type: stdio, http, streamable-http, sse
|
|
@@ -50,7 +55,7 @@ Options:
|
|
|
50
55
|
--sse-port=<port> SSE server port (default: 3002)
|
|
51
56
|
--http-host=<host> HTTP server host (default: 0.0.0.0)
|
|
52
57
|
--sse-host=<host> SSE server host (default: 0.0.0.0)
|
|
53
|
-
--btp=<destination> Override x-
|
|
58
|
+
--btp=<destination> Override x-sap-destination header (for BTP Cloud authorization)
|
|
54
59
|
Required for stdio/SSE transports unless provided in headers
|
|
55
60
|
--target-url=<url> Override target URL (separates Auth from Connection)
|
|
56
61
|
Useful for direct OData service testing or non-standard MCP URLs
|
|
@@ -77,6 +82,7 @@ Examples:
|
|
|
77
82
|
mcp-abap-adt-proxy --transport=sse --sse-port=3002 # SSE mode on port 3002
|
|
78
83
|
mcp-abap-adt-proxy --btp=ai # With BTP destination override
|
|
79
84
|
mcp-abap-adt-proxy --btp=ai --unsafe # With file-based session storage
|
|
85
|
+
mcp-abap-adt-proxy tui # Interactive configuration wizard
|
|
80
86
|
mcp-abap-adt-proxy --config=proxy-config.yaml # Load configuration from YAML file
|
|
81
87
|
mcp-abap-adt-proxy -c proxy-config.yml # Load configuration from YAML file (short form)
|
|
82
88
|
|
|
@@ -94,7 +100,7 @@ Usage Modes:
|
|
|
94
100
|
Example: mcp-abap-adt-proxy --btp=ai
|
|
95
101
|
|
|
96
102
|
Headers (for HTTP/SSE transports):
|
|
97
|
-
- x-
|
|
103
|
+
- x-sap-destination: Destination for BTP Cloud authorization (optional if --btp is used)
|
|
98
104
|
|
|
99
105
|
At least one of the above headers (or corresponding --btp parameter) is required.
|
|
100
106
|
|
|
@@ -137,6 +143,30 @@ function main() {
|
|
|
137
143
|
process.exit(0);
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
// Handle 'tui' subcommand — interactive configuration wizard
|
|
147
|
+
if (process.argv[2] === 'tui') {
|
|
148
|
+
const wizardPath = path.resolve(__dirname, '../dist/tui/index.js');
|
|
149
|
+
if (!fs.existsSync(wizardPath)) {
|
|
150
|
+
process.stderr.write(`[MCP Proxy] ✗ Wizard not found at: ${wizardPath}\n`);
|
|
151
|
+
process.stderr.write(`[MCP Proxy] Make sure to build the project with 'npm run build' first.\n`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
import(wizardPath).then(({ runWizard }) => {
|
|
156
|
+
runWizard().catch((err) => {
|
|
157
|
+
if (err.name === 'ExitPromptError') {
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
process.stderr.write(`[MCP Proxy] ✗ Wizard error: ${err.message}\n`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
163
|
+
}).catch((err) => {
|
|
164
|
+
process.stderr.write(`[MCP Proxy] ✗ Failed to load wizard: ${err.message}\n`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
140
170
|
// Spawn the main server process
|
|
141
171
|
const serverPath = path.resolve(__dirname, '../dist/index.js');
|
|
142
172
|
|
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare class McpAbapAdtProxyServer {
|
|
|
31
31
|
*/
|
|
32
32
|
private startHttpServer;
|
|
33
33
|
/**
|
|
34
|
-
* Handle proxy request -
|
|
34
|
+
* Handle proxy request - get JWT token and forward transparently via reverse proxy
|
|
35
35
|
*/
|
|
36
36
|
private handleProxyRequest;
|
|
37
37
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAuBH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAqBlC;;GAEG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,CAAW;gBAEhB,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAoDlE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAuE1B;;OAEG;YACW,eAAe;IAgG7B;;OAEG;YACW,kBAAkB;IA8ChC;;OAEG;YACW,cAAc;IA2T5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAmBhC;AAGD,eAAe,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const interfaces_1 = require("@mcp-abap-adt/interfaces");
|
|
|
15
15
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
16
16
|
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
17
17
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
18
|
+
const reverseProxy_js_1 = require("./proxy/reverseProxy.js");
|
|
18
19
|
const config_js_1 = require("./lib/config.js");
|
|
19
20
|
const logger_js_1 = require("./lib/logger.js");
|
|
20
21
|
const transportConfig_js_1 = require("./lib/transportConfig.js");
|
|
@@ -40,7 +41,17 @@ class McpAbapAdtProxyServer {
|
|
|
40
41
|
httpServer;
|
|
41
42
|
btpProxy;
|
|
42
43
|
constructor(transportConfig, configPath) {
|
|
43
|
-
|
|
44
|
+
// Load config first to extract transport overrides from YAML/JSON file
|
|
45
|
+
const resolvedConfigPath = configPath || (0, config_js_1.getConfigPath)();
|
|
46
|
+
let fileTransportOverrides;
|
|
47
|
+
if (resolvedConfigPath) {
|
|
48
|
+
const raw = (0, config_js_1.loadRawConfigFile)(resolvedConfigPath);
|
|
49
|
+
if (raw) {
|
|
50
|
+
fileTransportOverrides = (0, config_js_1.extractTransportFields)(raw);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.transportConfig =
|
|
54
|
+
transportConfig || (0, transportConfig_js_1.parseTransportConfig)(fileTransportOverrides);
|
|
44
55
|
this.config = (0, config_js_1.loadConfig)(configPath);
|
|
45
56
|
// Validate configuration
|
|
46
57
|
if (this.transportConfig.type === 'streamable-http' ||
|
|
@@ -145,9 +156,7 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
145
156
|
async startHttpServer() {
|
|
146
157
|
const httpConfig = this.transportConfig;
|
|
147
158
|
this.httpServer = (0, node_http_1.createServer)(async (req, res) => {
|
|
148
|
-
// Log incoming HTTP request
|
|
149
|
-
const _debugEnabled = process.env.DEBUG === 'true' ||
|
|
150
|
-
process.env.DEBUG_HTTP_REQUESTS === 'true';
|
|
159
|
+
// Log incoming HTTP request
|
|
151
160
|
const sanitizedHeaders = (0, requestInterceptor_js_1.sanitizeHeadersForLogging)(req.headers);
|
|
152
161
|
logger_js_1.logger?.info('=== HTTP REQUEST RECEIVED ===', {
|
|
153
162
|
type: 'HTTP_REQUEST_RECEIVED',
|
|
@@ -156,92 +165,14 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
156
165
|
headers: sanitizedHeaders,
|
|
157
166
|
remoteAddress: req.socket.remoteAddress,
|
|
158
167
|
});
|
|
159
|
-
//
|
|
160
|
-
// if (req.method !== 'POST') {
|
|
161
|
-
// logger?.warn('Non-POST request rejected', { ... });
|
|
162
|
-
// ...
|
|
163
|
-
// }
|
|
164
|
-
// Read request body
|
|
165
|
-
let body;
|
|
166
|
-
try {
|
|
167
|
-
const chunks = [];
|
|
168
|
-
for await (const chunk of req) {
|
|
169
|
-
chunks.push(chunk);
|
|
170
|
-
}
|
|
171
|
-
const bodyString = Buffer.concat(chunks).toString('utf-8');
|
|
172
|
-
logger_js_1.logger?.info('=== PARSING REQUEST BODY ===', {
|
|
173
|
-
type: 'REQUEST_BODY_PARSING',
|
|
174
|
-
bodyLength: bodyString.length,
|
|
175
|
-
bodyPreview: bodyString.substring(0, 200),
|
|
176
|
-
});
|
|
177
|
-
if (bodyString.trim().length > 0) {
|
|
178
|
-
body = JSON.parse(bodyString);
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
body = undefined;
|
|
182
|
-
}
|
|
183
|
-
// Sanitize body for logging
|
|
184
|
-
const sanitizedBody = {};
|
|
185
|
-
if (body && typeof body === 'object' && body !== null) {
|
|
186
|
-
const bodyObj = body;
|
|
187
|
-
sanitizedBody.method = bodyObj.method;
|
|
188
|
-
sanitizedBody.id = bodyObj.id;
|
|
189
|
-
sanitizedBody.jsonrpc = bodyObj.jsonrpc;
|
|
190
|
-
if (bodyObj.params &&
|
|
191
|
-
typeof bodyObj.params === 'object' &&
|
|
192
|
-
bodyObj.params !== null) {
|
|
193
|
-
const params = bodyObj.params;
|
|
194
|
-
sanitizedBody.params = {};
|
|
195
|
-
const sanitizedParams = sanitizedBody.params;
|
|
196
|
-
if (params.arguments &&
|
|
197
|
-
typeof params.arguments === 'object' &&
|
|
198
|
-
params.arguments !== null) {
|
|
199
|
-
sanitizedParams.arguments = {};
|
|
200
|
-
const sanitizedArgs = sanitizedParams.arguments;
|
|
201
|
-
for (const [key, value] of Object.entries(params.arguments)) {
|
|
202
|
-
const lowerKey = key.toLowerCase();
|
|
203
|
-
if (lowerKey.includes('password') ||
|
|
204
|
-
lowerKey.includes('token') ||
|
|
205
|
-
lowerKey.includes('secret')) {
|
|
206
|
-
sanitizedArgs[key] = '[REDACTED]';
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
sanitizedArgs[key] = value;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
for (const [key, value] of Object.entries(params)) {
|
|
214
|
-
if (key === 'arguments')
|
|
215
|
-
continue;
|
|
216
|
-
sanitizedParams[key] = value;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
logger_js_1.logger?.info('=== REQUEST BODY PARSED ===', {
|
|
221
|
-
type: 'REQUEST_BODY_PARSED',
|
|
222
|
-
body: sanitizedBody,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
logger_js_1.logger?.error('=== FAILED TO PARSE REQUEST BODY ===', {
|
|
227
|
-
type: 'REQUEST_PARSE_ERROR',
|
|
228
|
-
error: error instanceof Error ? error.message : String(error),
|
|
229
|
-
errorStack: error instanceof Error ? error.stack : undefined,
|
|
230
|
-
});
|
|
231
|
-
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
232
|
-
res.end('Invalid request body');
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
// Intercept and analyze request
|
|
168
|
+
// Intercept and analyze request (headers only, body is piped through)
|
|
236
169
|
const configOverrides = {
|
|
237
170
|
btpDestination: this.config.btpDestination,
|
|
238
171
|
targetUrl: this.config.targetUrl,
|
|
239
172
|
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
configOverrides,
|
|
173
|
+
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, undefined, configOverrides, {
|
|
174
|
+
skipHeaderValidation: true,
|
|
243
175
|
});
|
|
244
|
-
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
|
|
245
176
|
// Check routing decision
|
|
246
177
|
if (intercepted.routingDecision.strategy === headerAnalyzer_js_1.RoutingStrategy.UNKNOWN) {
|
|
247
178
|
logger_js_1.logger?.error('Routing decision failed', {
|
|
@@ -249,25 +180,16 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
249
180
|
reason: intercepted.routingDecision.reason,
|
|
250
181
|
});
|
|
251
182
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
252
|
-
const bodyId = body && typeof body === 'object' && body !== null && 'id' in body
|
|
253
|
-
? body.id
|
|
254
|
-
: null;
|
|
255
183
|
res.end(JSON.stringify({
|
|
256
|
-
|
|
257
|
-
id: bodyId || null,
|
|
258
|
-
error: {
|
|
259
|
-
code: -32602,
|
|
260
|
-
message: intercepted.routingDecision.reason,
|
|
261
|
-
},
|
|
184
|
+
error: intercepted.routingDecision.reason,
|
|
262
185
|
}));
|
|
263
186
|
return;
|
|
264
187
|
}
|
|
265
|
-
//
|
|
188
|
+
// Forward request via reverse proxy
|
|
266
189
|
logger_js_1.logger?.info('=== PROXYING REQUEST ===', {
|
|
267
190
|
type: 'PROXY_REQUEST_START',
|
|
268
191
|
btpDestination: intercepted.routingDecision.btpDestination,
|
|
269
192
|
});
|
|
270
|
-
// Handle proxy request - add JWT and forward to x-mcp-url
|
|
271
193
|
try {
|
|
272
194
|
await this.handleProxyRequest(intercepted, req, res);
|
|
273
195
|
}
|
|
@@ -278,8 +200,8 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
278
200
|
strategy: intercepted.routingDecision.strategy,
|
|
279
201
|
});
|
|
280
202
|
if (!res.headersSent) {
|
|
281
|
-
res.writeHead(500, { 'Content-Type': '
|
|
282
|
-
res.end('Internal server error');
|
|
203
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
204
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
283
205
|
}
|
|
284
206
|
}
|
|
285
207
|
});
|
|
@@ -311,192 +233,38 @@ Authorization source: ${authSourceObj}`;
|
|
|
311
233
|
});
|
|
312
234
|
}
|
|
313
235
|
/**
|
|
314
|
-
* Handle proxy request -
|
|
236
|
+
* Handle proxy request - get JWT token and forward transparently via reverse proxy
|
|
315
237
|
*/
|
|
316
238
|
async handleProxyRequest(intercepted, req, res) {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (lowerKey.includes('token') ||
|
|
323
|
-
lowerKey.includes('authorization') ||
|
|
324
|
-
lowerKey.includes('password') ||
|
|
325
|
-
lowerKey.includes('secret')) {
|
|
326
|
-
incomingHeaders[key] = '[REDACTED]';
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
incomingHeaders[key] = Array.isArray(value)
|
|
330
|
-
? value.join(', ')
|
|
331
|
-
: value || '';
|
|
332
|
-
}
|
|
239
|
+
const destination = intercepted.routingDecision.btpDestination;
|
|
240
|
+
if (!destination) {
|
|
241
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
242
|
+
res.end(JSON.stringify({ error: 'No BTP destination specified' }));
|
|
243
|
+
return;
|
|
333
244
|
}
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
intercepted.body !== null) {
|
|
338
|
-
const bodyObj = intercepted.body;
|
|
339
|
-
if (bodyObj.params &&
|
|
340
|
-
typeof bodyObj.params === 'object' &&
|
|
341
|
-
bodyObj.params !== null) {
|
|
342
|
-
const params = bodyObj.params;
|
|
343
|
-
sanitizedBody.params = {};
|
|
344
|
-
const sanitizedParams = sanitizedBody.params;
|
|
345
|
-
// For tools/call, log arguments
|
|
346
|
-
if (params.arguments &&
|
|
347
|
-
typeof params.arguments === 'object' &&
|
|
348
|
-
params.arguments !== null) {
|
|
349
|
-
sanitizedParams.arguments = {};
|
|
350
|
-
const sanitizedArgs = sanitizedParams.arguments;
|
|
351
|
-
for (const [key, value] of Object.entries(params.arguments)) {
|
|
352
|
-
const lowerKey = key.toLowerCase();
|
|
353
|
-
if (lowerKey.includes('password') ||
|
|
354
|
-
lowerKey.includes('token') ||
|
|
355
|
-
lowerKey.includes('secret')) {
|
|
356
|
-
sanitizedArgs[key] = '[REDACTED]';
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
sanitizedArgs[key] = value;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
// Log other params
|
|
364
|
-
for (const [key, value] of Object.entries(params)) {
|
|
365
|
-
if (key === 'arguments')
|
|
366
|
-
continue;
|
|
367
|
-
sanitizedParams[key] = value;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
sanitizedBody.method = bodyObj.method;
|
|
371
|
-
sanitizedBody.id = bodyObj.id;
|
|
372
|
-
sanitizedBody.jsonrpc = bodyObj.jsonrpc;
|
|
245
|
+
// Ensure proxy is initialized
|
|
246
|
+
if (!this.btpProxy) {
|
|
247
|
+
this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
|
|
373
248
|
}
|
|
374
|
-
// Use original request body structure for proxying (generic support)
|
|
375
|
-
// We do NOT enforce JSON-RPC structure here anymore, to support REST/OData
|
|
376
|
-
const proxyRequestData = intercepted.body;
|
|
377
|
-
logger_js_1.logger?.info('=== INCOMING REQUEST ===', {
|
|
378
|
-
type: 'PROXY_REQUEST_INCOMING',
|
|
379
|
-
method: req.method,
|
|
380
|
-
url: req.url,
|
|
381
|
-
btpDestination,
|
|
382
|
-
sessionId: intercepted.sessionId,
|
|
383
|
-
headers: incomingHeaders,
|
|
384
|
-
body: sanitizedBody,
|
|
385
|
-
mode: 'BTP authentication',
|
|
386
|
-
});
|
|
387
249
|
try {
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
const interceptedBodyIdType = typeof interceptedBodyId;
|
|
396
|
-
const interceptedBodyIdUndefined = interceptedBodyId === undefined;
|
|
397
|
-
logger_js_1.logger?.info('=== BEFORE BUILDING MCP REQUEST ===', {
|
|
398
|
-
type: 'BEFORE_BUILD_MCP_REQUEST',
|
|
399
|
-
interceptedBodyId,
|
|
400
|
-
interceptedBodyIdType,
|
|
401
|
-
interceptedBodyIdUndefined,
|
|
402
|
-
interceptedBodyIdNull: interceptedBodyId === null,
|
|
403
|
-
interceptedBodyIdZero: interceptedBodyId === 0,
|
|
404
|
-
interceptedBodyIdFalsy: !interceptedBodyId,
|
|
405
|
-
fullInterceptedBody: JSON.stringify(intercepted.body),
|
|
406
|
-
});
|
|
407
|
-
// Preserve id correctly - 0 is a valid id value
|
|
408
|
-
let mcpRequestId;
|
|
409
|
-
if (interceptedBodyId === undefined) {
|
|
410
|
-
mcpRequestId = null;
|
|
411
|
-
}
|
|
412
|
-
else if (typeof interceptedBodyId === 'string' ||
|
|
413
|
-
typeof interceptedBodyId === 'number') {
|
|
414
|
-
mcpRequestId = interceptedBodyId;
|
|
415
|
-
}
|
|
416
|
-
else {
|
|
417
|
-
mcpRequestId = null;
|
|
418
|
-
}
|
|
419
|
-
// Proxy request to BtpProxy
|
|
420
|
-
// Pass the original request data directly
|
|
421
|
-
const proxyRequest = {
|
|
422
|
-
method: intercepted.method, // Use original HTTP method (GET, POST, etc.)
|
|
423
|
-
url: intercepted.url, // Pass request URL (path)
|
|
424
|
-
data: proxyRequestData, // Use original body (or undefined)
|
|
425
|
-
id: mcpRequestId, // Keep ID for logging context if available
|
|
426
|
-
};
|
|
427
|
-
const proxyResponse = await this.btpProxy.proxyRequest(proxyRequest, intercepted.routingDecision, intercepted.headers);
|
|
428
|
-
// Fix: If backend returned null ID but we entered with a valid ID (e.g. 0), restore it.
|
|
429
|
-
// Cline/Zod schemas reject null ID for successful responses if request had an ID.
|
|
430
|
-
if ((proxyResponse.id === null || proxyResponse.id === undefined) &&
|
|
431
|
-
mcpRequestId !== null &&
|
|
432
|
-
mcpRequestId !== undefined) {
|
|
433
|
-
logger_js_1.logger?.debug('Restoring original request ID in response', {
|
|
434
|
-
originalId: mcpRequestId,
|
|
435
|
-
receivedId: proxyResponse.id,
|
|
436
|
-
});
|
|
437
|
-
proxyResponse.id = mcpRequestId;
|
|
438
|
-
}
|
|
439
|
-
logger_js_1.logger?.info('=== FORWARDING REQUEST ===', {
|
|
440
|
-
type: 'PROXY_REQUEST_FORWARDING',
|
|
441
|
-
interceptedBodyIdAtForward: getBodyId(intercepted.body),
|
|
442
|
-
proxyRequest,
|
|
443
|
-
routingDecision: intercepted.routingDecision,
|
|
444
|
-
headers: intercepted.headers,
|
|
445
|
-
});
|
|
446
|
-
// Log response
|
|
447
|
-
const sanitizedResponse = {
|
|
448
|
-
jsonrpc: proxyResponse.jsonrpc,
|
|
449
|
-
id: proxyResponse.id,
|
|
450
|
-
};
|
|
451
|
-
if (proxyResponse.result) {
|
|
452
|
-
sanitizedResponse.result = proxyResponse.result;
|
|
453
|
-
}
|
|
454
|
-
if (proxyResponse.error) {
|
|
455
|
-
sanitizedResponse.error = {
|
|
456
|
-
code: proxyResponse.error.code,
|
|
457
|
-
message: proxyResponse.error.message,
|
|
458
|
-
data: proxyResponse.error.data,
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
logger_js_1.logger?.info('=== RESPONSE FROM TARGET SERVICE ===', {
|
|
462
|
-
type: 'PROXY_RESPONSE_RECEIVED',
|
|
463
|
-
response: sanitizedResponse,
|
|
464
|
-
hasResult: !!proxyResponse.result,
|
|
465
|
-
hasError: !!proxyResponse.error,
|
|
466
|
-
});
|
|
467
|
-
// Send response back to client
|
|
468
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
469
|
-
res.end(JSON.stringify(proxyResponse));
|
|
470
|
-
logger_js_1.logger?.info('=== RESPONSE SENT TO CLIENT ===', {
|
|
471
|
-
type: 'PROXY_RESPONSE_SENT',
|
|
472
|
-
statusCode: 200,
|
|
473
|
-
});
|
|
474
|
-
logger_js_1.logger?.debug('BtpProxy request completed', {
|
|
475
|
-
type: 'BTP_PROXY_COMPLETED',
|
|
476
|
-
hasResult: !!proxyResponse.result,
|
|
477
|
-
hasError: !!proxyResponse.error,
|
|
478
|
-
});
|
|
250
|
+
// Get JWT token (cached, auto-refresh)
|
|
251
|
+
const jwtToken = await this.btpProxy.getJwtToken(destination);
|
|
252
|
+
// Get target URL
|
|
253
|
+
const targetUrl = intercepted.routingDecision.targetUrl ||
|
|
254
|
+
(await this.btpProxy.getTargetUrl(destination));
|
|
255
|
+
// Forward request transparently
|
|
256
|
+
await (0, reverseProxy_js_1.forwardRequest)(req, res, targetUrl, jwtToken);
|
|
479
257
|
}
|
|
480
258
|
catch (error) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
error:
|
|
485
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
259
|
+
logger_js_1.logger?.error('Proxy request failed', {
|
|
260
|
+
type: 'PROXY_REQUEST_ERROR',
|
|
261
|
+
destination,
|
|
262
|
+
error: error instanceof Error ? error.message : String(error),
|
|
486
263
|
});
|
|
487
|
-
// Output error to stderr for user visibility (only if verbose mode is enabled)
|
|
488
|
-
if ((0, btpProxy_js_1.shouldWriteStderr)()) {
|
|
489
|
-
process.stderr.write(`[MCP Proxy] ✗ Connection error: ${errorMessage}\n`);
|
|
490
|
-
}
|
|
491
264
|
if (!res.headersSent) {
|
|
492
|
-
res.writeHead(
|
|
265
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
493
266
|
res.end(JSON.stringify({
|
|
494
|
-
|
|
495
|
-
id: getBodyId(intercepted.body) || null,
|
|
496
|
-
error: {
|
|
497
|
-
code: -32000,
|
|
498
|
-
message: errorMessage,
|
|
499
|
-
},
|
|
267
|
+
error: error instanceof Error ? error.message : 'Proxy error',
|
|
500
268
|
}));
|
|
501
269
|
}
|
|
502
270
|
}
|
|
@@ -686,6 +454,7 @@ Authorization source: ${authSourceObj}`;
|
|
|
686
454
|
// Intercept and analyze request with config overrides
|
|
687
455
|
const configOverrides = {
|
|
688
456
|
btpDestination: this.config.btpDestination,
|
|
457
|
+
targetUrl: this.config.targetUrl,
|
|
689
458
|
};
|
|
690
459
|
const intercepted = (0, requestInterceptor_js_1.interceptRequest)(req, body, configOverrides);
|
|
691
460
|
// Check routing decision
|
|
@@ -704,9 +473,19 @@ Authorization source: ${authSourceObj}`;
|
|
|
704
473
|
}));
|
|
705
474
|
return;
|
|
706
475
|
}
|
|
707
|
-
// Handle proxy request
|
|
476
|
+
// Handle proxy request via BtpProxy (JSON-RPC mode for SSE)
|
|
708
477
|
try {
|
|
709
|
-
|
|
478
|
+
if (!this.btpProxy) {
|
|
479
|
+
this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
|
|
480
|
+
}
|
|
481
|
+
const proxyResponse = await this.btpProxy.proxyRequest({
|
|
482
|
+
method: intercepted.method,
|
|
483
|
+
url: intercepted.url,
|
|
484
|
+
data: body,
|
|
485
|
+
id: getBodyId(body),
|
|
486
|
+
}, intercepted.routingDecision, intercepted.headers);
|
|
487
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
488
|
+
res.end(JSON.stringify(proxyResponse));
|
|
710
489
|
}
|
|
711
490
|
catch (error) {
|
|
712
491
|
logger_js_1.logger?.error('Failed to process SSE POST request', {
|