@mcp-abap-adt/proxy 1.2.0 → 1.3.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 +22 -0
- package/README.md +4 -20
- package/bin/mcp-abap-adt-proxy.js +4 -34
- package/dist/index.js +81 -2
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +66 -18
- package/dist/lib/transportConfig.js +2 -2
- package/dist/proxy/btpProxy.d.ts.map +1 -1
- package/dist/proxy/btpProxy.js +2 -0
- package/package.json +1 -3
- package/dist/tui/index.d.ts +0 -8
- package/dist/tui/index.d.ts.map +0 -1
- package/dist/tui/index.js +0 -11
- package/dist/tui/serviceKeyCheck.d.ts +0 -25
- package/dist/tui/serviceKeyCheck.d.ts.map +0 -1
- package/dist/tui/serviceKeyCheck.js +0 -62
- package/dist/tui/wizard.d.ts +0 -38
- package/dist/tui/wizard.d.ts.map +0 -1
- package/dist/tui/wizard.js +0 -321
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2026-05-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **CLI flags override values from `--config`** — when a config file is loaded via `--config`/`-c`, explicit CLI flags (`--btp`, `--target-url`/`--url`, `--unsafe`, `--browser`, `--browser-auth-port`, `--header`) now override matching values from the file instead of being silently dropped. `--header` entries merge with `defaultHeaders` (CLI keys win on conflict). The proxy logs which keys were overridden so the precedence is visible.
|
|
14
|
+
- **`--help` / `-h`** — full CLI usage printer covering auth, browser, transport flags and the documented environment variables. Previously the proxy had no help output at all, making the `--browser none` / `--browser-auth-port` flags effectively undiscoverable.
|
|
15
|
+
- **Graceful shutdown on `SIGINT` / `SIGTERM`** — the proxy now wires both signals to `shutdown()`, which closes the MCP server and HTTP listener, clears pending JWT refresh timers, drops the token cache, and releases per-destination auth brokers. Previously these resources were only released on hard process exit.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `BtpProxy.dispose()` now clears the token cache and the per-destination broker map in addition to refresh timers, so a clean shutdown actually releases all auth state.
|
|
19
|
+
|
|
20
|
+
## [1.2.1] - 2026-05-26
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
- **Default bind host changed from `0.0.0.0` to `127.0.0.1`** (loopback) for HTTP and SSE transports. The proxy holds auth tokens and runs locally, so it no longer listens on all network interfaces by default. Set `httpHost`/`sseHost` (or `MCP_HTTP_HOST`/`MCP_SSE_HOST`) to `0.0.0.0` to expose it intentionally.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
- **Interactive configuration wizard (TUI)** — removed the `mcp-abap-adt-proxy tui` subcommand and the `src/tui/` module (its `direct`/`mcpUrl` scenario no longer matched the BTP-only proxy and produced unusable configs). Client and proxy configuration is handled by the separate `@mcp-abap-adt/configurator` package (`mcp-conf`). Dropped the `@inquirer/prompts` dependency.
|
|
27
|
+
- **Combined-launch test harness** — removed `tools/start-servers.js` and the `npm run test:servers` script. It started a local `mcp-abap-adt` server with the proxy in front of it via the long-removed `--mcp-url`/`--mcp` flags, which no longer applies to a pure authorization proxy.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **Documentation synced to the BTP-only model.** Removed stale references to the long-gone `x-mcp-url`/`--mcp-url`, `mcpDestination`, `cloudLlmHubUrl`/`CLOUD_LLM_HUB_URL`, `DEBUG_HTTP_REQUESTS`, and config-file auto-discovery/`MCP_PROXY_CONFIG`. Documented the actual options: `defaultHeaders`, `browser`/`browserAuthPort`, and `targetUrl`/`x-target-url`. Removed the obsolete `IMPLEMENTATION_ANALYSIS.md`.
|
|
31
|
+
|
|
10
32
|
## [1.2.0] - 2026-04-20
|
|
11
33
|
|
|
12
34
|
### Added
|
package/README.md
CHANGED
|
@@ -142,7 +142,6 @@ The proxy uses BTP/XSUAA authentication:
|
|
|
142
142
|
- **[Architecture](./docs/ARCHITECTURE.md)** - System architecture and design
|
|
143
143
|
- **[Troubleshooting](./docs/TROUBLESHOOTING.md)** - Common issues and solutions
|
|
144
144
|
- **[Routing Logic Specification](./docs/ROUTING_LOGIC.md)** - Detailed routing logic and scenarios
|
|
145
|
-
- **[Implementation Analysis](./docs/IMPLEMENTATION_ANALYSIS.md)** - Current implementation analysis
|
|
146
145
|
- **[Roadmap](./ROADMAP.md)** - Development roadmap and progress
|
|
147
146
|
|
|
148
147
|
## How It Works
|
|
@@ -226,28 +225,13 @@ See [Configuration Guide](./docs/CONFIGURATION.md) for complete options.
|
|
|
226
225
|
|
|
227
226
|
## Testing Tools
|
|
228
227
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Use the included script to start both `mcp-abap-adt` and `mcp-abap-adt-proxy` simultaneously:
|
|
228
|
+
Verify a BTP destination's service key and token retrieval:
|
|
232
229
|
|
|
233
230
|
```bash
|
|
234
|
-
|
|
235
|
-
npm run test:servers
|
|
236
|
-
|
|
237
|
-
# Direct execution
|
|
238
|
-
node tools/start-servers.js
|
|
239
|
-
|
|
240
|
-
# With SSE transport
|
|
241
|
-
node tools/start-servers.js --transport=sse
|
|
231
|
+
npm run test-destination
|
|
242
232
|
```
|
|
243
233
|
|
|
244
|
-
|
|
245
|
-
- Starts ADT server on port 3000 (HTTP) or 3001 (SSE)
|
|
246
|
-
- Starts Proxy server on port 3001 (HTTP) or 3002 (SSE)
|
|
247
|
-
- Generates `mcpUrl` based on ADT server configuration
|
|
248
|
-
- Ensures both servers use the same transport protocol
|
|
249
|
-
|
|
250
|
-
See [tools/README.md](./tools/README.md) for complete documentation.
|
|
234
|
+
See [tools/README.md](./tools/README.md) for the available scripts.
|
|
251
235
|
|
|
252
236
|
## Development Status
|
|
253
237
|
|
|
@@ -258,7 +242,7 @@ See [tools/README.md](./tools/README.md) for complete documentation.
|
|
|
258
242
|
- ✅ JWT Token Management & Proxy Forwarding
|
|
259
243
|
- ✅ Configuration & Environment
|
|
260
244
|
- ✅ Error Handling & Resilience
|
|
261
|
-
- ✅ Testing Tools (`tools
|
|
245
|
+
- ✅ Testing Tools (`tools/`)
|
|
262
246
|
- ✅ Documentation
|
|
263
247
|
|
|
264
248
|
🚧 **Future Work**
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* mcp-abap-adt-proxy [options]
|
|
10
|
-
* mcp-abap-adt-proxy tui
|
|
11
10
|
* mcp-abap-adt-proxy --transport=stdio
|
|
12
11
|
* mcp-abap-adt-proxy --transport=http
|
|
13
12
|
*/
|
|
@@ -44,17 +43,13 @@ MCP ABAP ADT Proxy Server v${pkg.version}
|
|
|
44
43
|
|
|
45
44
|
Usage:
|
|
46
45
|
mcp-abap-adt-proxy [options]
|
|
47
|
-
mcp-abap-adt-proxy tui
|
|
48
|
-
|
|
49
|
-
Commands:
|
|
50
|
-
tui Interactive configuration wizard (generates YAML config)
|
|
51
46
|
|
|
52
47
|
Options:
|
|
53
48
|
--transport=<type> Transport type: stdio, http, streamable-http, sse
|
|
54
49
|
--http-port=<port> HTTP server port (default: 3001)
|
|
55
50
|
--sse-port=<port> SSE server port (default: 3002)
|
|
56
|
-
--http-host=<host> HTTP server host (default:
|
|
57
|
-
--sse-host=<host> SSE server host (default:
|
|
51
|
+
--http-host=<host> HTTP server host (default: 127.0.0.1)
|
|
52
|
+
--sse-host=<host> SSE server host (default: 127.0.0.1)
|
|
58
53
|
--btp=<destination> Override x-sap-destination header (for BTP Cloud authorization)
|
|
59
54
|
Required for stdio/SSE transports unless provided in headers
|
|
60
55
|
--target-url=<url> Override target URL (separates Auth from Connection)
|
|
@@ -70,8 +65,8 @@ Options:
|
|
|
70
65
|
Environment Variables:
|
|
71
66
|
MCP_HTTP_PORT HTTP server port (default: 3001)
|
|
72
67
|
MCP_SSE_PORT SSE server port (default: 3002)
|
|
73
|
-
MCP_HTTP_HOST HTTP server host (default:
|
|
74
|
-
MCP_SSE_HOST SSE server host (default:
|
|
68
|
+
MCP_HTTP_HOST HTTP server host (default: 127.0.0.1)
|
|
69
|
+
MCP_SSE_HOST SSE server host (default: 127.0.0.1)
|
|
75
70
|
MCP_TRANSPORT Transport type (stdio|http|sse)
|
|
76
71
|
MCP_PROXY_UNSAFE Enable file-based session storage (set to "true")
|
|
77
72
|
|
|
@@ -85,7 +80,6 @@ Examples:
|
|
|
85
80
|
mcp-abap-adt-proxy --btp=ai # With BTP destination override
|
|
86
81
|
mcp-abap-adt-proxy --btp=ai --unsafe # With file-based session storage
|
|
87
82
|
mcp-abap-adt-proxy --btp=ai --header x-sap-destination=S4HANA # With default SAP headers
|
|
88
|
-
mcp-abap-adt-proxy tui # Interactive configuration wizard
|
|
89
83
|
mcp-abap-adt-proxy --config=proxy-config.yaml # Load configuration from YAML file
|
|
90
84
|
mcp-abap-adt-proxy -c proxy-config.yml # Load configuration from YAML file (short form)
|
|
91
85
|
|
|
@@ -146,30 +140,6 @@ function main() {
|
|
|
146
140
|
process.exit(0);
|
|
147
141
|
}
|
|
148
142
|
|
|
149
|
-
// Handle 'tui' subcommand — interactive configuration wizard
|
|
150
|
-
if (process.argv[2] === 'tui') {
|
|
151
|
-
const wizardPath = path.resolve(__dirname, '../dist/tui/index.js');
|
|
152
|
-
if (!fs.existsSync(wizardPath)) {
|
|
153
|
-
process.stderr.write(`[MCP Proxy] ✗ Wizard not found at: ${wizardPath}\n`);
|
|
154
|
-
process.stderr.write(`[MCP Proxy] Make sure to build the project with 'npm run build' first.\n`);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
import(wizardPath).then(({ runWizard }) => {
|
|
159
|
-
runWizard().catch((err) => {
|
|
160
|
-
if (err.name === 'ExitPromptError') {
|
|
161
|
-
process.exit(0);
|
|
162
|
-
}
|
|
163
|
-
process.stderr.write(`[MCP Proxy] ✗ Wizard error: ${err.message}\n`);
|
|
164
|
-
process.exit(1);
|
|
165
|
-
});
|
|
166
|
-
}).catch((err) => {
|
|
167
|
-
process.stderr.write(`[MCP Proxy] ✗ Failed to load wizard: ${err.message}\n`);
|
|
168
|
-
process.exit(1);
|
|
169
|
-
});
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
143
|
// Spawn the main server process
|
|
174
144
|
const serverPath = path.resolve(__dirname, '../dist/index.js');
|
|
175
145
|
|
package/dist/index.js
CHANGED
|
@@ -206,7 +206,7 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
206
206
|
}
|
|
207
207
|
});
|
|
208
208
|
const port = httpConfig.port || 3001;
|
|
209
|
-
const host = httpConfig.host || '
|
|
209
|
+
const host = httpConfig.host || '127.0.0.1';
|
|
210
210
|
return new Promise((resolve, reject) => {
|
|
211
211
|
this.httpServer?.listen(port, host, () => {
|
|
212
212
|
const authSourceObj = this.config.btpDestination
|
|
@@ -513,7 +513,7 @@ Authorization source: ${authSourceObj}`;
|
|
|
513
513
|
res.end('Not found');
|
|
514
514
|
});
|
|
515
515
|
const port = sseConfig.port || 3002;
|
|
516
|
-
const host = sseConfig.host || '
|
|
516
|
+
const host = sseConfig.host || '127.0.0.1';
|
|
517
517
|
return new Promise((resolve, reject) => {
|
|
518
518
|
httpServer.listen(port, host, () => {
|
|
519
519
|
const authSourceObj = this.config.btpDestination
|
|
@@ -567,9 +567,88 @@ Authorization source: ${authSourceObj}`;
|
|
|
567
567
|
exports.McpAbapAdtProxyServer = McpAbapAdtProxyServer;
|
|
568
568
|
// Export for use in bin script
|
|
569
569
|
exports.default = McpAbapAdtProxyServer;
|
|
570
|
+
/**
|
|
571
|
+
* Print CLI usage and exit.
|
|
572
|
+
*/
|
|
573
|
+
function printHelp() {
|
|
574
|
+
const help = `Usage: mcp-abap-adt-proxy [options]
|
|
575
|
+
|
|
576
|
+
Authorization proxy for MCP clients (Cline, Claude Code, etc.) that cannot
|
|
577
|
+
authenticate on their own to MCP servers deployed on SAP BTP.
|
|
578
|
+
|
|
579
|
+
CONFIGURATION
|
|
580
|
+
-c, --config <path> Load config from YAML/JSON file. CLI flags below
|
|
581
|
+
override values from the file.
|
|
582
|
+
|
|
583
|
+
AUTHENTICATION (BTP / XSUAA)
|
|
584
|
+
--btp <destination> BTP destination name. Service key is read from
|
|
585
|
+
~/.config/mcp-abap-adt/service-keys/<dest>.json
|
|
586
|
+
--target-url <url> Override target service URL (default: from service key)
|
|
587
|
+
--url <url> Alias for --target-url
|
|
588
|
+
--unsafe Persist session/tokens to disk (default: in-memory)
|
|
589
|
+
--header <key=value> Inject a default header into every forwarded
|
|
590
|
+
request. Repeatable. Client-supplied headers win.
|
|
591
|
+
|
|
592
|
+
BROWSER AUTHORIZATION
|
|
593
|
+
--browser <mode> Browser used for OAuth login. One of:
|
|
594
|
+
system - OS default browser (default)
|
|
595
|
+
chrome - Google Chrome
|
|
596
|
+
edge - Microsoft Edge
|
|
597
|
+
firefox - Firefox
|
|
598
|
+
none - Print URL to console and wait for
|
|
599
|
+
callback (for SSH/headless sessions)
|
|
600
|
+
headless - Alias for 'none'
|
|
601
|
+
--browser-auth-port <port> Local OAuth callback port. Must match the
|
|
602
|
+
redirect_uri registered on XSUAA (default: 3333).
|
|
603
|
+
|
|
604
|
+
TRANSPORT
|
|
605
|
+
--transport <type> stdio | streamable-http | http | sse
|
|
606
|
+
(auto-detected from stdin TTY if not set)
|
|
607
|
+
--stdio Shortcut for --transport stdio
|
|
608
|
+
--http Shortcut for --transport streamable-http
|
|
609
|
+
--sse Shortcut for --transport sse
|
|
610
|
+
--http-port <port> HTTP transport port (default: 3001)
|
|
611
|
+
--http-host <host> HTTP transport host (default: 127.0.0.1)
|
|
612
|
+
--sse-port <port> SSE transport port (default: 3002)
|
|
613
|
+
--sse-host <host> SSE transport host (default: 127.0.0.1)
|
|
614
|
+
|
|
615
|
+
-h, --help Show this help and exit
|
|
616
|
+
|
|
617
|
+
ENVIRONMENT VARIABLES
|
|
618
|
+
MCP_TRANSPORT, MCP_HTTP_PORT, MCP_HTTP_HOST, MCP_SSE_PORT, MCP_SSE_HOST,
|
|
619
|
+
LOG_LEVEL, MCP_PROXY_UNSAFE, MCP_PROXY_MAX_RETRIES, MCP_PROXY_RETRY_DELAY,
|
|
620
|
+
MCP_PROXY_REQUEST_TIMEOUT, MCP_PROXY_CIRCUIT_BREAKER_THRESHOLD,
|
|
621
|
+
MCP_PROXY_CIRCUIT_BREAKER_TIMEOUT
|
|
622
|
+
|
|
623
|
+
See docs/CONFIGURATION.md for the full configuration reference.
|
|
624
|
+
`;
|
|
625
|
+
process.stdout.write(help);
|
|
626
|
+
}
|
|
570
627
|
// Auto-start server when run directly (unless MCP_SKIP_AUTO_START is set)
|
|
571
628
|
if (process.env.MCP_SKIP_AUTO_START !== 'true') {
|
|
629
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
630
|
+
printHelp();
|
|
631
|
+
process.exit(0);
|
|
632
|
+
}
|
|
572
633
|
const server = new McpAbapAdtProxyServer();
|
|
634
|
+
// Graceful shutdown: release the OAuth callback port, MCP server, and HTTP listener.
|
|
635
|
+
let shuttingDown = false;
|
|
636
|
+
const onSignal = (signal) => {
|
|
637
|
+
if (shuttingDown)
|
|
638
|
+
return;
|
|
639
|
+
shuttingDown = true;
|
|
640
|
+
logger_js_1.logger?.info('Received shutdown signal, releasing resources', { signal });
|
|
641
|
+
server
|
|
642
|
+
.shutdown()
|
|
643
|
+
.catch((error) => {
|
|
644
|
+
logger_js_1.logger?.error('Error during shutdown', {
|
|
645
|
+
error: error instanceof Error ? error.message : String(error),
|
|
646
|
+
});
|
|
647
|
+
})
|
|
648
|
+
.finally(() => process.exit(0));
|
|
649
|
+
};
|
|
650
|
+
process.on('SIGINT', onSignal);
|
|
651
|
+
process.on('SIGTERM', onSignal);
|
|
573
652
|
server.run().catch((error) => {
|
|
574
653
|
logger_js_1.logger?.error('Fatal error while running MCP proxy server', {
|
|
575
654
|
type: 'SERVER_FATAL_ERROR',
|
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;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,
|
|
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,CAyB3D;AAwED;;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
|
@@ -56,24 +56,17 @@ function loadConfig(configPath) {
|
|
|
56
56
|
// Get config path from parameter or command line (--config/-c)
|
|
57
57
|
// Do NOT use environment variable MCP_PROXY_CONFIG - only explicit --config parameter
|
|
58
58
|
const finalConfigPath = configPath || getConfigPath();
|
|
59
|
-
// If --config is provided
|
|
59
|
+
// If --config is provided: load from file, apply defaults, then overlay explicit CLI flags.
|
|
60
|
+
// CLI flags override YAML values (YAML is just the baseline).
|
|
60
61
|
if (finalConfigPath) {
|
|
61
|
-
// Warn if other CLI parameters are also provided (they will be ignored)
|
|
62
|
-
const conflictingParams = ['--btp', '--unsafe'].filter((param) => hasArg(param));
|
|
63
|
-
if (conflictingParams.length > 0) {
|
|
64
|
-
console.warn(`Warning: --config is specified, but the following CLI parameters will be ignored: ${conflictingParams.join(', ')}. ` +
|
|
65
|
-
`Configuration will be loaded ONLY from ${finalConfigPath}. ` +
|
|
66
|
-
`To use CLI parameters, do not specify --config.`);
|
|
67
|
-
}
|
|
68
62
|
try {
|
|
69
|
-
if (fs.existsSync(finalConfigPath)) {
|
|
70
|
-
const fileConfig = loadConfigFile(finalConfigPath);
|
|
71
|
-
// Apply defaults for missing values in file config
|
|
72
|
-
return applyDefaults(fileConfig);
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
63
|
+
if (!fs.existsSync(finalConfigPath)) {
|
|
75
64
|
throw new Error(`Config file not found: ${finalConfigPath}`);
|
|
76
65
|
}
|
|
66
|
+
const fileConfig = loadConfigFile(finalConfigPath);
|
|
67
|
+
const base = applyDefaults(fileConfig);
|
|
68
|
+
const cli = readCliOverrides();
|
|
69
|
+
return mergeCliOverrides(base, cli, finalConfigPath);
|
|
77
70
|
}
|
|
78
71
|
catch (error) {
|
|
79
72
|
throw new Error(`Failed to load config from file ${finalConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -82,6 +75,61 @@ function loadConfig(configPath) {
|
|
|
82
75
|
// No --config provided: load ONLY from command line parameters and environment variables
|
|
83
76
|
return loadFromEnv();
|
|
84
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Read CLI flags that should override file/YAML configuration.
|
|
80
|
+
* Only includes keys that were explicitly provided on the command line.
|
|
81
|
+
*/
|
|
82
|
+
function readCliOverrides() {
|
|
83
|
+
const overrides = {};
|
|
84
|
+
const btp = getArgValue('--btp');
|
|
85
|
+
if (btp !== undefined)
|
|
86
|
+
overrides.btpDestination = btp;
|
|
87
|
+
const targetUrl = getArgValue('--target-url') ?? getArgValue('--url');
|
|
88
|
+
if (targetUrl !== undefined)
|
|
89
|
+
overrides.targetUrl = targetUrl;
|
|
90
|
+
if (hasArg('--unsafe'))
|
|
91
|
+
overrides.unsafe = true;
|
|
92
|
+
const browser = getArgValue('--browser');
|
|
93
|
+
if (browser !== undefined) {
|
|
94
|
+
overrides.browser = browser;
|
|
95
|
+
}
|
|
96
|
+
const browserAuthPortStr = getArgValue('--browser-auth-port');
|
|
97
|
+
if (browserAuthPortStr !== undefined) {
|
|
98
|
+
overrides.browserAuthPort = parseInt(browserAuthPortStr, 10);
|
|
99
|
+
}
|
|
100
|
+
const headerArgs = getAllArgValues('--header');
|
|
101
|
+
if (headerArgs.length > 0) {
|
|
102
|
+
const headers = {};
|
|
103
|
+
for (const arg of headerArgs) {
|
|
104
|
+
const eqIndex = arg.indexOf('=');
|
|
105
|
+
if (eqIndex > 0) {
|
|
106
|
+
const key = arg.substring(0, eqIndex).toLowerCase();
|
|
107
|
+
const value = arg.substring(eqIndex + 1);
|
|
108
|
+
headers[key] = value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
overrides.defaultHeaders = headers;
|
|
112
|
+
}
|
|
113
|
+
return overrides;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Merge CLI overrides on top of file-based config. Logs which keys were overridden.
|
|
117
|
+
* `defaultHeaders` is merged per-key (CLI keys override YAML keys); other fields are replaced.
|
|
118
|
+
*/
|
|
119
|
+
function mergeCliOverrides(base, cli, configPath) {
|
|
120
|
+
const merged = { ...base, ...cli };
|
|
121
|
+
if (cli.defaultHeaders) {
|
|
122
|
+
merged.defaultHeaders = {
|
|
123
|
+
...(base.defaultHeaders || {}),
|
|
124
|
+
...cli.defaultHeaders,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const overriddenKeys = Object.keys(cli);
|
|
128
|
+
if (overriddenKeys.length > 0) {
|
|
129
|
+
console.warn(`Note: CLI flags override values from ${configPath}: ${overriddenKeys.join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
return merged;
|
|
132
|
+
}
|
|
85
133
|
/**
|
|
86
134
|
* Extract transport-related fields from a raw config object
|
|
87
135
|
*/
|
|
@@ -132,8 +180,8 @@ function applyDefaults(fileConfig) {
|
|
|
132
180
|
const result = {
|
|
133
181
|
httpPort: fileConfig.httpPort ?? 3001,
|
|
134
182
|
ssePort: fileConfig.ssePort ?? 3002,
|
|
135
|
-
httpHost: fileConfig.httpHost || '
|
|
136
|
-
sseHost: fileConfig.sseHost || '
|
|
183
|
+
httpHost: fileConfig.httpHost || '127.0.0.1',
|
|
184
|
+
sseHost: fileConfig.sseHost || '127.0.0.1',
|
|
137
185
|
logLevel: fileConfig.logLevel || 'info',
|
|
138
186
|
btpDestination: fileConfig.btpDestination,
|
|
139
187
|
targetUrl: fileConfig.targetUrl,
|
|
@@ -179,8 +227,8 @@ function loadFromEnv() {
|
|
|
179
227
|
return {
|
|
180
228
|
httpPort: parseInt(process.env.MCP_HTTP_PORT || '3001', 10),
|
|
181
229
|
ssePort: parseInt(process.env.MCP_SSE_PORT || '3002', 10),
|
|
182
|
-
httpHost: process.env.MCP_HTTP_HOST || '
|
|
183
|
-
sseHost: process.env.MCP_SSE_HOST || '
|
|
230
|
+
httpHost: process.env.MCP_HTTP_HOST || '127.0.0.1',
|
|
231
|
+
sseHost: process.env.MCP_SSE_HOST || '127.0.0.1',
|
|
184
232
|
logLevel: process.env.LOG_LEVEL || 'info',
|
|
185
233
|
btpDestination,
|
|
186
234
|
targetUrl,
|
|
@@ -64,7 +64,7 @@ function parseTransportConfig(fileOverrides) {
|
|
|
64
64
|
const host = getArgValue('--sse-host') ||
|
|
65
65
|
fileOverrides?.sseHost ||
|
|
66
66
|
process.env.MCP_SSE_HOST ||
|
|
67
|
-
'
|
|
67
|
+
'127.0.0.1';
|
|
68
68
|
return {
|
|
69
69
|
type: 'sse',
|
|
70
70
|
host,
|
|
@@ -85,7 +85,7 @@ function parseTransportConfig(fileOverrides) {
|
|
|
85
85
|
const host = getArgValue('--http-host') ||
|
|
86
86
|
fileOverrides?.httpHost ||
|
|
87
87
|
process.env.MCP_HTTP_HOST ||
|
|
88
|
-
'
|
|
88
|
+
'127.0.0.1';
|
|
89
89
|
return {
|
|
90
90
|
type: 'streamable-http',
|
|
91
91
|
host,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"btpProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/btpProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAgB,MAAM,2BAA2B,CAAC;AAYrE,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AA4CnE;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAU3C;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAGD,MAAM,MAAM,YAAY,GAAG,mBAAmB,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,UAAU,CACN;IACZ,OAAO,CAAC,aAAa,CAA0C;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAU;gBAEZ,oBAAoB,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAqF3E;;;OAGG;IACU,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB3D;;;;OAIG;YACW,wBAAwB;IAqFtC;;OAEG;YACW,uBAAuB;IAyFrC;;;;OAIG;IACG,WAAW,CACf,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,MAAM,CAAC;IAiHlB;;OAEG;IACH,OAAO,CAAC,YAAY;IAapB;;;OAGG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA6BhC;;OAEG;YACW,gBAAgB;IAU9B;;OAEG;IACI,OAAO,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"btpProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/btpProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAgB,MAAM,2BAA2B,CAAC;AAYrE,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AA4CnE;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAU3C;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAGD,MAAM,MAAM,YAAY,GAAG,mBAAmB,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,UAAU,CACN;IACZ,OAAO,CAAC,aAAa,CAA0C;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAU;gBAEZ,oBAAoB,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAqF3E;;;OAGG;IACU,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB3D;;;;OAIG;YACW,wBAAwB;IAqFtC;;OAEG;YACW,uBAAuB;IAyFrC;;;;OAIG;IACG,WAAW,CACf,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,MAAM,CAAC;IAiHlB;;OAEG;IACH,OAAO,CAAC,YAAY;IAapB;;;OAGG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA6BhC;;OAEG;YACW,gBAAgB;IAU9B;;OAEG;IACI,OAAO,IAAI,IAAI;IAStB;;;OAGG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAexD;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;;;;;;;;;;;;;;;OAgBG;YACW,iBAAiB;IAmT/B;;OAEG;IACG,YAAY,CAChB,eAAe,EAAE,YAAY,EAC7B,eAAe,EAAE,eAAe,EAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAC7D,OAAO,CAAC,aAAa,CAAC;IAiLzB;;OAEG;WACiB,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;CAyB7E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAEnB"}
|
package/dist/proxy/btpProxy.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"dev": "npx @modelcontextprotocol/inspector node ./dist/index.js",
|
|
20
20
|
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
21
21
|
"prepublishOnly": "npm run build",
|
|
22
|
-
"test:servers": "node ./tools/start-servers.js",
|
|
23
22
|
"test-destination": "npx tsx tools/test-destination.ts"
|
|
24
23
|
},
|
|
25
24
|
"keywords": [
|
|
@@ -64,7 +63,6 @@
|
|
|
64
63
|
"typescript": "^6.0.3"
|
|
65
64
|
},
|
|
66
65
|
"dependencies": {
|
|
67
|
-
"@inquirer/prompts": "^8.4.2",
|
|
68
66
|
"@mcp-abap-adt/auth-broker": "^1.0.5",
|
|
69
67
|
"@mcp-abap-adt/auth-providers": "^1.0.5",
|
|
70
68
|
"@mcp-abap-adt/auth-stores": "^1.0.4",
|
package/dist/tui/index.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI module barrel export
|
|
3
|
-
*/
|
|
4
|
-
export type { ServiceKeyCheckResult } from './serviceKeyCheck.js';
|
|
5
|
-
export { checkServiceKeyExists } from './serviceKeyCheck.js';
|
|
6
|
-
export type { WizardAnswers } from './wizard.js';
|
|
7
|
-
export { generateConfigYaml, runWizard } from './wizard.js';
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/tui/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tui/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/tui/index.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* TUI module barrel export
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.runWizard = exports.generateConfigYaml = exports.checkServiceKeyExists = void 0;
|
|
7
|
-
var serviceKeyCheck_js_1 = require("./serviceKeyCheck.js");
|
|
8
|
-
Object.defineProperty(exports, "checkServiceKeyExists", { enumerable: true, get: function () { return serviceKeyCheck_js_1.checkServiceKeyExists; } });
|
|
9
|
-
var wizard_js_1 = require("./wizard.js");
|
|
10
|
-
Object.defineProperty(exports, "generateConfigYaml", { enumerable: true, get: function () { return wizard_js_1.generateConfigYaml; } });
|
|
11
|
-
Object.defineProperty(exports, "runWizard", { enumerable: true, get: function () { return wizard_js_1.runWizard; } });
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Service key existence checker
|
|
3
|
-
* Checks if a service key file exists for a given BTP destination
|
|
4
|
-
* by searching platform-specific directories.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Result of a service key existence check
|
|
8
|
-
*/
|
|
9
|
-
export interface ServiceKeyCheckResult {
|
|
10
|
-
/** Whether the service key file was found */
|
|
11
|
-
found: boolean;
|
|
12
|
-
/** Absolute path to the found service key file (if found) */
|
|
13
|
-
path?: string;
|
|
14
|
-
/** Directories that were searched (when not found) */
|
|
15
|
-
searchedPaths?: string[];
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Check if a service key file exists for the given BTP destination.
|
|
19
|
-
* Searches all platform-specific directories returned by getPlatformPaths('service-keys').
|
|
20
|
-
*
|
|
21
|
-
* @param destination - BTP destination name (without .json extension)
|
|
22
|
-
* @returns ServiceKeyCheckResult indicating whether the key was found and where
|
|
23
|
-
*/
|
|
24
|
-
export declare function checkServiceKeyExists(destination: string): ServiceKeyCheckResult;
|
|
25
|
-
//# sourceMappingURL=serviceKeyCheck.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"serviceKeyCheck.d.ts","sourceRoot":"","sources":["../../src/tui/serviceKeyCheck.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,6CAA6C;IAC7C,KAAK,EAAE,OAAO,CAAC;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,GAClB,qBAAqB,CAYvB"}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Service key existence checker
|
|
4
|
-
* Checks if a service key file exists for a given BTP destination
|
|
5
|
-
* by searching platform-specific directories.
|
|
6
|
-
*/
|
|
7
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
-
if (k2 === undefined) k2 = k;
|
|
9
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
-
}
|
|
13
|
-
Object.defineProperty(o, k2, desc);
|
|
14
|
-
}) : (function(o, m, k, k2) {
|
|
15
|
-
if (k2 === undefined) k2 = k;
|
|
16
|
-
o[k2] = m[k];
|
|
17
|
-
}));
|
|
18
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
-
}) : function(o, v) {
|
|
21
|
-
o["default"] = v;
|
|
22
|
-
});
|
|
23
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
-
var ownKeys = function(o) {
|
|
25
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
-
var ar = [];
|
|
27
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
-
return ar;
|
|
29
|
-
};
|
|
30
|
-
return ownKeys(o);
|
|
31
|
-
};
|
|
32
|
-
return function (mod) {
|
|
33
|
-
if (mod && mod.__esModule) return mod;
|
|
34
|
-
var result = {};
|
|
35
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
-
__setModuleDefault(result, mod);
|
|
37
|
-
return result;
|
|
38
|
-
};
|
|
39
|
-
})();
|
|
40
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
-
exports.checkServiceKeyExists = checkServiceKeyExists;
|
|
42
|
-
const fs = __importStar(require("node:fs"));
|
|
43
|
-
const path = __importStar(require("node:path"));
|
|
44
|
-
const stores_js_1 = require("../lib/stores.js");
|
|
45
|
-
/**
|
|
46
|
-
* Check if a service key file exists for the given BTP destination.
|
|
47
|
-
* Searches all platform-specific directories returned by getPlatformPaths('service-keys').
|
|
48
|
-
*
|
|
49
|
-
* @param destination - BTP destination name (without .json extension)
|
|
50
|
-
* @returns ServiceKeyCheckResult indicating whether the key was found and where
|
|
51
|
-
*/
|
|
52
|
-
function checkServiceKeyExists(destination) {
|
|
53
|
-
const searchDirs = (0, stores_js_1.getPlatformPaths)('service-keys');
|
|
54
|
-
const fileName = `${destination}.json`;
|
|
55
|
-
for (const dir of searchDirs) {
|
|
56
|
-
const filePath = path.join(dir, fileName);
|
|
57
|
-
if (fs.existsSync(filePath)) {
|
|
58
|
-
return { found: true, path: filePath };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return { found: false, searchedPaths: searchDirs };
|
|
62
|
-
}
|
package/dist/tui/wizard.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI wizard for interactive proxy configuration
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Answers collected by the wizard
|
|
6
|
-
*/
|
|
7
|
-
export interface WizardAnswers {
|
|
8
|
-
scenario: 'btp' | 'direct';
|
|
9
|
-
btpDestination?: string;
|
|
10
|
-
targetUrl?: string;
|
|
11
|
-
mcpDestination?: string;
|
|
12
|
-
mcpUrl?: string;
|
|
13
|
-
transport: 'stdio' | 'http' | 'sse';
|
|
14
|
-
httpHost?: string;
|
|
15
|
-
httpPort?: number;
|
|
16
|
-
sseHost?: string;
|
|
17
|
-
ssePort?: number;
|
|
18
|
-
browser: 'system' | 'headless' | 'chrome' | 'edge' | 'firefox' | 'none';
|
|
19
|
-
browserAuthPort: number;
|
|
20
|
-
unsafe: boolean;
|
|
21
|
-
logLevel: string;
|
|
22
|
-
maxRetries?: number;
|
|
23
|
-
retryDelay?: number;
|
|
24
|
-
requestTimeout?: number;
|
|
25
|
-
circuitBreakerThreshold?: number;
|
|
26
|
-
circuitBreakerTimeout?: number;
|
|
27
|
-
cloudLlmHubUrl?: string;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Generate a YAML configuration string from wizard answers.
|
|
31
|
-
* The output follows the structure of docs/mcp-proxy-config.example.yaml.
|
|
32
|
-
*/
|
|
33
|
-
export declare function generateConfigYaml(answers: WizardAnswers): string;
|
|
34
|
-
/**
|
|
35
|
-
* Run the interactive configuration wizard
|
|
36
|
-
*/
|
|
37
|
-
export declare function runWizard(): Promise<WizardAnswers>;
|
|
38
|
-
//# sourceMappingURL=wizard.d.ts.map
|
package/dist/tui/wizard.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"wizard.d.ts","sourceRoot":"","sources":["../../src/tui/wizard.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,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;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAWD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CA+FjE;AAeD;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,aAAa,CAAC,CAwMxD"}
|
package/dist/tui/wizard.js
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* TUI wizard for interactive proxy configuration
|
|
4
|
-
*/
|
|
5
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
-
if (k2 === undefined) k2 = k;
|
|
7
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
-
}
|
|
11
|
-
Object.defineProperty(o, k2, desc);
|
|
12
|
-
}) : (function(o, m, k, k2) {
|
|
13
|
-
if (k2 === undefined) k2 = k;
|
|
14
|
-
o[k2] = m[k];
|
|
15
|
-
}));
|
|
16
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
-
}) : function(o, v) {
|
|
19
|
-
o["default"] = v;
|
|
20
|
-
});
|
|
21
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
-
var ownKeys = function(o) {
|
|
23
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
-
var ar = [];
|
|
25
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
-
return ar;
|
|
27
|
-
};
|
|
28
|
-
return ownKeys(o);
|
|
29
|
-
};
|
|
30
|
-
return function (mod) {
|
|
31
|
-
if (mod && mod.__esModule) return mod;
|
|
32
|
-
var result = {};
|
|
33
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
-
__setModuleDefault(result, mod);
|
|
35
|
-
return result;
|
|
36
|
-
};
|
|
37
|
-
})();
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.generateConfigYaml = generateConfigYaml;
|
|
40
|
-
exports.runWizard = runWizard;
|
|
41
|
-
const fs = __importStar(require("node:fs"));
|
|
42
|
-
const serviceKeyCheck_js_1 = require("./serviceKeyCheck.js");
|
|
43
|
-
/** Default values for advanced settings */
|
|
44
|
-
const DEFAULTS = {
|
|
45
|
-
maxRetries: 3,
|
|
46
|
-
retryDelay: 1000,
|
|
47
|
-
requestTimeout: 60000,
|
|
48
|
-
circuitBreakerThreshold: 5,
|
|
49
|
-
circuitBreakerTimeout: 60000,
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Generate a YAML configuration string from wizard answers.
|
|
53
|
-
* The output follows the structure of docs/mcp-proxy-config.example.yaml.
|
|
54
|
-
*/
|
|
55
|
-
function generateConfigYaml(answers) {
|
|
56
|
-
const lines = [];
|
|
57
|
-
lines.push('# MCP ABAP ADT Proxy Configuration');
|
|
58
|
-
lines.push('');
|
|
59
|
-
// Transport configuration
|
|
60
|
-
lines.push('# Transport configuration');
|
|
61
|
-
lines.push(`transport: ${answers.transport} # stdio | http | sse`);
|
|
62
|
-
if (answers.transport === 'http' || answers.transport === 'sse') {
|
|
63
|
-
lines.push(`httpPort: ${answers.httpPort ?? 3001}`);
|
|
64
|
-
lines.push(`httpHost: "${answers.httpHost ?? '0.0.0.0'}"`);
|
|
65
|
-
}
|
|
66
|
-
if (answers.transport === 'sse') {
|
|
67
|
-
lines.push(`ssePort: ${answers.ssePort ?? 3002}`);
|
|
68
|
-
lines.push(`sseHost: "${answers.sseHost ?? '0.0.0.0'}"`);
|
|
69
|
-
}
|
|
70
|
-
lines.push('');
|
|
71
|
-
// Destination overrides
|
|
72
|
-
lines.push('# Destination overrides');
|
|
73
|
-
if (answers.scenario === 'btp') {
|
|
74
|
-
if (answers.btpDestination) {
|
|
75
|
-
lines.push(`btpDestination: "${answers.btpDestination}"`);
|
|
76
|
-
}
|
|
77
|
-
if (answers.targetUrl) {
|
|
78
|
-
lines.push(`targetUrl: "${answers.targetUrl}"`);
|
|
79
|
-
}
|
|
80
|
-
if (answers.mcpDestination) {
|
|
81
|
-
lines.push(`mcpDestination: "${answers.mcpDestination}"`);
|
|
82
|
-
}
|
|
83
|
-
if (answers.mcpUrl) {
|
|
84
|
-
lines.push(`# mcpUrl: "${answers.mcpUrl}"`);
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
lines.push('# mcpUrl:');
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
lines.push('# btpDestination:');
|
|
92
|
-
lines.push('# mcpDestination:');
|
|
93
|
-
if (answers.mcpUrl) {
|
|
94
|
-
lines.push(`mcpUrl: "${answers.mcpUrl}"`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
lines.push('');
|
|
98
|
-
// Browser authentication
|
|
99
|
-
lines.push('# Browser authentication');
|
|
100
|
-
lines.push(`browser: "${answers.browser}"`);
|
|
101
|
-
lines.push(`browserAuthPort: ${answers.browserAuthPort}`);
|
|
102
|
-
lines.push('');
|
|
103
|
-
// Session storage mode
|
|
104
|
-
lines.push('# Session storage mode');
|
|
105
|
-
lines.push(`unsafe: ${answers.unsafe} # If true, persists tokens to disk`);
|
|
106
|
-
lines.push('');
|
|
107
|
-
// Error handling & resilience
|
|
108
|
-
lines.push('# Error handling & resilience');
|
|
109
|
-
lines.push(`maxRetries: ${answers.maxRetries ?? DEFAULTS.maxRetries}`);
|
|
110
|
-
lines.push(`retryDelay: ${answers.retryDelay ?? DEFAULTS.retryDelay} # milliseconds`);
|
|
111
|
-
lines.push(`requestTimeout: ${answers.requestTimeout ?? DEFAULTS.requestTimeout} # milliseconds`);
|
|
112
|
-
lines.push(`circuitBreakerThreshold: ${answers.circuitBreakerThreshold ?? DEFAULTS.circuitBreakerThreshold}`);
|
|
113
|
-
lines.push(`circuitBreakerTimeout: ${answers.circuitBreakerTimeout ?? DEFAULTS.circuitBreakerTimeout} # milliseconds`);
|
|
114
|
-
lines.push('');
|
|
115
|
-
// Logging
|
|
116
|
-
lines.push('# Logging');
|
|
117
|
-
lines.push(`logLevel: "${answers.logLevel}" # debug | info | warn | error`);
|
|
118
|
-
// Cloud LLM Hub URL
|
|
119
|
-
if (answers.cloudLlmHubUrl) {
|
|
120
|
-
lines.push('');
|
|
121
|
-
lines.push('# Cloud LLM Hub URL');
|
|
122
|
-
lines.push(`cloudLlmHubUrl: "${answers.cloudLlmHubUrl}"`);
|
|
123
|
-
}
|
|
124
|
-
lines.push('');
|
|
125
|
-
return lines.join('\n');
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Print a service key check result to the console.
|
|
129
|
-
*/
|
|
130
|
-
function printServiceKeyCheck(destination) {
|
|
131
|
-
const result = (0, serviceKeyCheck_js_1.checkServiceKeyExists)(destination);
|
|
132
|
-
if (result.found) {
|
|
133
|
-
console.log(` Found: ${result.path}`);
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
console.log(` Warning: Service key file not found for "${destination}".`);
|
|
137
|
-
console.log(` Searched: ${result.searchedPaths?.join(', ') ?? '(none)'}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Run the interactive configuration wizard
|
|
142
|
-
*/
|
|
143
|
-
async function runWizard() {
|
|
144
|
-
try {
|
|
145
|
-
const { select, input, confirm, number } = await import('@inquirer/prompts');
|
|
146
|
-
console.log('\n MCP ABAP ADT Proxy — Configuration Wizard\n');
|
|
147
|
-
// 1. Scenario
|
|
148
|
-
const scenario = await select({
|
|
149
|
-
message: 'Select scenario:',
|
|
150
|
-
choices: [
|
|
151
|
-
{
|
|
152
|
-
name: 'BTP service (XSUAA auth via service key)',
|
|
153
|
-
value: 'btp',
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: 'Direct MCP server URL (no auth)',
|
|
157
|
-
value: 'direct',
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
});
|
|
161
|
-
const answers = { scenario };
|
|
162
|
-
// 2. Scenario-specific questions
|
|
163
|
-
if (scenario === 'btp') {
|
|
164
|
-
const btpDestination = await input({
|
|
165
|
-
message: 'BTP destination name:',
|
|
166
|
-
required: true,
|
|
167
|
-
});
|
|
168
|
-
answers.btpDestination = btpDestination;
|
|
169
|
-
printServiceKeyCheck(btpDestination);
|
|
170
|
-
const targetUrl = await input({
|
|
171
|
-
message: 'Target service URL (leave empty if in service key):',
|
|
172
|
-
default: '',
|
|
173
|
-
});
|
|
174
|
-
if (targetUrl) {
|
|
175
|
-
answers.targetUrl = targetUrl;
|
|
176
|
-
}
|
|
177
|
-
const addMcpDest = await confirm({
|
|
178
|
-
message: 'Add MCP destination (for ABAP connection params on Cloud)?',
|
|
179
|
-
default: false,
|
|
180
|
-
});
|
|
181
|
-
if (addMcpDest) {
|
|
182
|
-
const mcpDestination = await input({
|
|
183
|
-
message: 'MCP destination name:',
|
|
184
|
-
required: true,
|
|
185
|
-
});
|
|
186
|
-
answers.mcpDestination = mcpDestination;
|
|
187
|
-
printServiceKeyCheck(mcpDestination);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
const mcpUrl = await input({
|
|
192
|
-
message: 'MCP server URL:',
|
|
193
|
-
required: true,
|
|
194
|
-
});
|
|
195
|
-
answers.mcpUrl = mcpUrl;
|
|
196
|
-
}
|
|
197
|
-
// 3. Transport
|
|
198
|
-
const transport = await select({
|
|
199
|
-
message: 'Select transport:',
|
|
200
|
-
choices: [
|
|
201
|
-
{ name: 'stdio', value: 'stdio' },
|
|
202
|
-
{ name: 'streamable-http', value: 'http' },
|
|
203
|
-
{ name: 'sse', value: 'sse' },
|
|
204
|
-
],
|
|
205
|
-
});
|
|
206
|
-
answers.transport = transport;
|
|
207
|
-
// 4. Transport-specific settings
|
|
208
|
-
if (transport !== 'stdio') {
|
|
209
|
-
answers.httpHost = await input({
|
|
210
|
-
message: 'HTTP host:',
|
|
211
|
-
default: '0.0.0.0',
|
|
212
|
-
});
|
|
213
|
-
answers.httpPort = await number({
|
|
214
|
-
message: 'HTTP port:',
|
|
215
|
-
default: 3001,
|
|
216
|
-
min: 1,
|
|
217
|
-
max: 65535,
|
|
218
|
-
});
|
|
219
|
-
if (transport === 'sse') {
|
|
220
|
-
answers.sseHost = await input({
|
|
221
|
-
message: 'SSE host:',
|
|
222
|
-
default: '0.0.0.0',
|
|
223
|
-
});
|
|
224
|
-
answers.ssePort = await number({
|
|
225
|
-
message: 'SSE port:',
|
|
226
|
-
default: 3002,
|
|
227
|
-
min: 1,
|
|
228
|
-
max: 65535,
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// 5. Browser
|
|
233
|
-
answers.browser = await select({
|
|
234
|
-
message: 'Select browser for OAuth2 login:',
|
|
235
|
-
choices: [
|
|
236
|
-
{ name: 'system (default browser)', value: 'system' },
|
|
237
|
-
{ name: 'chrome', value: 'chrome' },
|
|
238
|
-
{ name: 'edge', value: 'edge' },
|
|
239
|
-
{ name: 'firefox', value: 'firefox' },
|
|
240
|
-
{ name: 'headless', value: 'headless' },
|
|
241
|
-
{ name: 'none (disable)', value: 'none' },
|
|
242
|
-
],
|
|
243
|
-
});
|
|
244
|
-
// 6. OAuth2 callback port
|
|
245
|
-
answers.browserAuthPort = await number({
|
|
246
|
-
message: 'OAuth2 callback port:',
|
|
247
|
-
default: 3333,
|
|
248
|
-
});
|
|
249
|
-
// 7. Unsafe mode
|
|
250
|
-
answers.unsafe = await confirm({
|
|
251
|
-
message: 'Unsafe mode (persist tokens to disk)?',
|
|
252
|
-
default: false,
|
|
253
|
-
});
|
|
254
|
-
// 8. Log level
|
|
255
|
-
answers.logLevel = await select({
|
|
256
|
-
message: 'Log level:',
|
|
257
|
-
choices: [
|
|
258
|
-
{ name: 'debug', value: 'debug' },
|
|
259
|
-
{ name: 'info', value: 'info' },
|
|
260
|
-
{ name: 'warn', value: 'warn' },
|
|
261
|
-
{ name: 'error', value: 'error' },
|
|
262
|
-
],
|
|
263
|
-
default: 'info',
|
|
264
|
-
});
|
|
265
|
-
// 9. Advanced settings
|
|
266
|
-
const configureAdvanced = await confirm({
|
|
267
|
-
message: 'Configure advanced settings?',
|
|
268
|
-
default: false,
|
|
269
|
-
});
|
|
270
|
-
if (configureAdvanced) {
|
|
271
|
-
answers.maxRetries = await number({
|
|
272
|
-
message: 'Max retries:',
|
|
273
|
-
default: DEFAULTS.maxRetries,
|
|
274
|
-
});
|
|
275
|
-
answers.retryDelay = await number({
|
|
276
|
-
message: 'Retry delay (ms):',
|
|
277
|
-
default: DEFAULTS.retryDelay,
|
|
278
|
-
});
|
|
279
|
-
answers.requestTimeout = await number({
|
|
280
|
-
message: 'Request timeout (ms):',
|
|
281
|
-
default: DEFAULTS.requestTimeout,
|
|
282
|
-
});
|
|
283
|
-
answers.circuitBreakerThreshold = await number({
|
|
284
|
-
message: 'Circuit breaker threshold:',
|
|
285
|
-
default: DEFAULTS.circuitBreakerThreshold,
|
|
286
|
-
});
|
|
287
|
-
answers.circuitBreakerTimeout = await number({
|
|
288
|
-
message: 'Circuit breaker timeout (ms):',
|
|
289
|
-
default: DEFAULTS.circuitBreakerTimeout,
|
|
290
|
-
});
|
|
291
|
-
const setCloudUrl = await confirm({
|
|
292
|
-
message: 'Set Cloud LLM Hub URL?',
|
|
293
|
-
default: false,
|
|
294
|
-
});
|
|
295
|
-
if (setCloudUrl) {
|
|
296
|
-
answers.cloudLlmHubUrl = await input({
|
|
297
|
-
message: 'Cloud LLM Hub URL:',
|
|
298
|
-
required: true,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
// 10. Output file
|
|
303
|
-
const outputPath = await input({
|
|
304
|
-
message: 'Output file path:',
|
|
305
|
-
default: './mcp-proxy-config.yaml',
|
|
306
|
-
});
|
|
307
|
-
// Generate and write config
|
|
308
|
-
const fullAnswers = answers;
|
|
309
|
-
const yaml = generateConfigYaml(fullAnswers);
|
|
310
|
-
fs.writeFileSync(outputPath, yaml, 'utf-8');
|
|
311
|
-
console.log(`\nConfig saved to: ${outputPath}`);
|
|
312
|
-
console.log(`Run with: mcp-abap-adt-proxy --config ${outputPath}`);
|
|
313
|
-
return fullAnswers;
|
|
314
|
-
}
|
|
315
|
-
catch (error) {
|
|
316
|
-
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
317
|
-
process.exit(0);
|
|
318
|
-
}
|
|
319
|
-
throw error;
|
|
320
|
-
}
|
|
321
|
-
}
|