@mcp-abap-adt/proxy 4.1.1 → 5.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 +100 -0
- package/README.md +3 -3
- package/bin/mcp-abap-adt-proxy-mcp.js +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -33
- package/dist/lib/authFailure.d.ts +36 -0
- package/dist/lib/authFailure.d.ts.map +1 -0
- package/dist/lib/authFailure.js +113 -0
- package/dist/mcp/supervisor.d.ts +9 -0
- package/dist/mcp/supervisor.d.ts.map +1 -1
- package/dist/mcp/supervisor.js +4 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +5 -0
- package/dist/proxy/btpProxy.d.ts +11 -14
- package/dist/proxy/btpProxy.d.ts.map +1 -1
- package/dist/proxy/btpProxy.js +75 -194
- package/dist/proxy/targetUrlSessionStore.d.ts +32 -0
- package/dist/proxy/targetUrlSessionStore.d.ts.map +1 -0
- package/dist/proxy/targetUrlSessionStore.js +62 -0
- package/docs/CONFIGURATION.md +7 -1
- package/docs/TROUBLESHOOTING.md +33 -19
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,106 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.0.0] - 2026-09-26
|
|
11
|
+
|
|
12
|
+
**The proxy moves onto the new auth family, and its tree holds one copy of each
|
|
13
|
+
contract package.** `npm ls` shows exactly one version each of
|
|
14
|
+
`interfaces-auth` (2.1.0), `interfaces-auth-sap` (1.0.1), `interfaces-network`
|
|
15
|
+
(2.0.0), `auth-providers` (4.2.0) and `auth-stores` (1.2.3).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **BREAKING: Node.js 22 or 24** — `engines: "^22 || ^24"` (was `>=18.2.0`).
|
|
20
|
+
`@mcp-abap-adt/auth-providers` 3.0.0 requires it, following the versions SAP
|
|
21
|
+
BTP's Cloud Foundry Node.js buildpack offers.
|
|
22
|
+
|
|
23
|
+
**Migrating:** run the proxy on Node 22 or 24. Nothing in its configuration
|
|
24
|
+
or CLI changed.
|
|
25
|
+
- **`@mcp-abap-adt/auth-broker` `^3.0.0`** (was `^2.2.0`, a major).
|
|
26
|
+
`BtpProxy` builds its brokers the new way —
|
|
27
|
+
`new AuthBroker({ serviceKeyStore, sessionStore, provider }, logger)` — with
|
|
28
|
+
a provider *factory*, so the broker seeds each destination's
|
|
29
|
+
`AuthorizationCodeProvider` with the refresh token and the access token the
|
|
30
|
+
session stored. The login is still a browser callback on `browserAuthPort`
|
|
31
|
+
(default 3333) with a five-minute window; `--browser none` / `headless` still
|
|
32
|
+
prints the URL instead of opening a browser.
|
|
33
|
+
- **`@mcp-abap-adt/auth-providers` `^4.2.0`** (was `^2.2.2`). The SAML changes
|
|
34
|
+
of 3.x and 4.x do not reach this package, which builds only
|
|
35
|
+
`AuthorizationCodeProvider`. A failed browser login is now
|
|
36
|
+
`BrowserAuthError` (a plain `Error` before).
|
|
37
|
+
- `@mcp-abap-adt/interfaces-auth` `^2.1.0` (was `^1.2.0`),
|
|
38
|
+
`@mcp-abap-adt/interfaces-auth-sap` `^1.0.1`, `@mcp-abap-adt/auth-stores`
|
|
39
|
+
`^1.2.3`, `@mcp-abap-adt/header-validator` `^0.3.1`,
|
|
40
|
+
`@mcp-abap-adt/connection` `^9.3.0` — the releases that accept
|
|
41
|
+
`interfaces-auth` 2, so none of them carries a private copy of it. None
|
|
42
|
+
changes an API this package uses.
|
|
43
|
+
- **With `--unsafe`, the session file no longer holds the client secret.**
|
|
44
|
+
auth-broker 3 writes the token and the refresh token only, and the proxy no
|
|
45
|
+
longer writes UAA credentials into the session either: with a `targetUrl`
|
|
46
|
+
it used to put the service key's client ID and secret there — or
|
|
47
|
+
`placeholder` credentials when it could not find them — through a private
|
|
48
|
+
broker method. The secret stays in the service key.
|
|
49
|
+
- **With `--unsafe`, the session file keeps its own URL.** The proxy also used
|
|
50
|
+
to write its `targetUrl` into the destination's session before building the
|
|
51
|
+
broker, so the broker could resolve a `serviceUrl` an XSUAA service key does
|
|
52
|
+
not carry. That session is the file `mcp-auth` writes too
|
|
53
|
+
(`~/.config/mcp-abap-adt/sessions/<destination>.env`), and its `SAP_URL`
|
|
54
|
+
was replaced by the MCP server's URL. The broker now reads the target URL
|
|
55
|
+
through `TargetUrlSessionStore`, a wrapper around the session store: reads
|
|
56
|
+
answer the target URL, writes keep the URL the session already holds, and
|
|
57
|
+
only a session that does not exist yet is created with the target URL.
|
|
58
|
+
Measured on the real `AbapSessionStore`: after a token write the file's
|
|
59
|
+
`SAP_URL` is unchanged.
|
|
60
|
+
- **Auth failures reach the log as the provider raised them.** auth-broker 3
|
|
61
|
+
no longer rewraps them into `Token provider … error for <destination>`. The
|
|
62
|
+
reason the standalone proxy prints before exiting is now chosen by error
|
|
63
|
+
class and `code` through the whole `cause` chain (`ServiceKeyNotFoundError`
|
|
64
|
+
and `ValidationError` → service key, `ECONNREFUSED` and kin → network), and
|
|
65
|
+
by message only where nothing typed says more (a login timeout, a UAA
|
|
66
|
+
refusal).
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- **An unreadable service key is reported as what it is.** The stores answer a
|
|
71
|
+
missing file with `null` and throw only when the file is there and wrong —
|
|
72
|
+
not valid JSON, not readable. The proxy answered every such throw as "service
|
|
73
|
+
key not found", with advice to create a file that already existed. The
|
|
74
|
+
store's error now reaches the caller as raised, naming the file; only `null`
|
|
75
|
+
means missing. Found in review.
|
|
76
|
+
- **`getAuthorizationHeader` passes the failure on as the same object.** It
|
|
77
|
+
wrapped it in a new `Error` with the same message, so `instanceof` on the
|
|
78
|
+
caller's side saw a plain `Error`, whatever the provider or store raised.
|
|
79
|
+
|
|
80
|
+
- **A missing service key is reported as one again.** The proxy recognised it
|
|
81
|
+
by the `.env` / `mcp.env` wording of auth-broker 2's message, which 3.0 no
|
|
82
|
+
longer produces — it would have said only that the session lacks a
|
|
83
|
+
`serviceUrl`. The proxy now checks the stores itself before building a
|
|
84
|
+
broker and raises its own `ServiceKeyNotFoundError`, with the same
|
|
85
|
+
"Service key file not found for destination … Please create service key
|
|
86
|
+
file: <destination>.json" message as before. It is still not retried.
|
|
87
|
+
- **`npm run test-destination` works.** It imported `XsuaaTokenProvider` and
|
|
88
|
+
`BtpTokenProvider`, removed from auth-providers long ago, and printed the
|
|
89
|
+
first 50 characters of the token. It now goes through `BtpProxy` itself and
|
|
90
|
+
prints the header's length only:
|
|
91
|
+
`npm run test-destination -- <destination> [--target-url <url>]`.
|
|
92
|
+
|
|
93
|
+
## [4.2.0] - 2026-09-24
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
|
|
97
|
+
- **`host` on `proxy_start`**, and the config's `httpHost` is honoured. The
|
|
98
|
+
default when neither says anything is still `127.0.0.1`.
|
|
99
|
+
|
|
100
|
+
It was ignored, on the reasoning that a proxy a model started should not be
|
|
101
|
+
network-reachable. That reasoning does not hold: loopback is a default, not a
|
|
102
|
+
boundary — anything with a shell can forward a port, so refusing to bind
|
|
103
|
+
elsewhere stopped only the honest case. What the default still does is keep a
|
|
104
|
+
proxy nobody asked to publish from becoming reachable without someone saying
|
|
105
|
+
so, which is worth a default and not worth a restriction.
|
|
106
|
+
|
|
107
|
+
Precedence, most specific first: the `host` argument, the config's `httpHost`,
|
|
108
|
+
then `127.0.0.1`.
|
|
109
|
+
|
|
10
110
|
## [4.1.1] - 2026-09-24
|
|
11
111
|
|
|
12
112
|
### Changed
|
package/README.md
CHANGED
|
@@ -283,7 +283,7 @@ Create `mcp-proxy-config.json`:
|
|
|
283
283
|
|
|
284
284
|
**Session Storage:**
|
|
285
285
|
- `unsafe: false` (default) - Session data stored in-memory (secure, lost on restart)
|
|
286
|
-
- `unsafe: true` - Session data persisted to disk (tokens saved under the session store path)
|
|
286
|
+
- `unsafe: true` - Session data persisted to disk (tokens saved under the session store path): the service URL, the token and the refresh token. The client secret is not copied there; it stays in the service key
|
|
287
287
|
|
|
288
288
|
See [Configuration Guide](./docs/CONFIGURATION.md) for complete options.
|
|
289
289
|
|
|
@@ -296,7 +296,7 @@ See [Configuration Guide](./docs/CONFIGURATION.md) for complete options.
|
|
|
296
296
|
|
|
297
297
|
## Requirements
|
|
298
298
|
|
|
299
|
-
- Node.js
|
|
299
|
+
- Node.js 22 or 24 (`engines: "^22 || ^24"`, required by `@mcp-abap-adt/auth-providers`)
|
|
300
300
|
- npm >= 9.0.0
|
|
301
301
|
|
|
302
302
|
## Testing Tools
|
|
@@ -304,7 +304,7 @@ See [Configuration Guide](./docs/CONFIGURATION.md) for complete options.
|
|
|
304
304
|
Verify a BTP destination's service key and token retrieval:
|
|
305
305
|
|
|
306
306
|
```bash
|
|
307
|
-
npm run test-destination
|
|
307
|
+
npm run test-destination -- <destination>
|
|
308
308
|
```
|
|
309
309
|
|
|
310
310
|
See [tools/README.md](./tools/README.md) for the available scripts.
|
|
@@ -41,7 +41,8 @@ Tools offered to the client:
|
|
|
41
41
|
List the environments — one SAP system's credentials each,
|
|
42
42
|
from sessions/. Variable NAMES only, never values.
|
|
43
43
|
proxy_start Start a proxy from a config plus, when the config needs one,
|
|
44
|
-
an environment.
|
|
44
|
+
an environment. Binds 127.0.0.1 unless the config's httpHost or
|
|
45
|
+
a host argument says otherwise. The config supplies
|
|
45
46
|
the destination, target URL, default headers and timeouts;
|
|
46
47
|
the PORT does not come from it — a free one is bound, so
|
|
47
48
|
several proxies can run at once, and the URL returned is the
|
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;AAwBH,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;IAE5B,OAAO,CAAC,iBAAiB,CAAC,CAA+C;IAEzE;;;;;;;OAOG;IACH,OAAO,KAAK,YAAY,GAevB;IACD,OAAO,CAAC,WAAW,CAAS;gBAEhB,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAoDlE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA2E1B;;OAEG;YACW,eAAe;IAiE7B;;OAEG;YACW,cAAc;IAiV5B;;;;;OAKG;YACW,gBAAgB;IAiC9B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBhC;AAGD,eAAe,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
18
18
|
const requestHandler_js_1 = require("./proxy/requestHandler.js");
|
|
19
19
|
const reverseProxy_js_1 = require("./proxy/reverseProxy.js");
|
|
20
20
|
const config_js_1 = require("./lib/config.js");
|
|
21
|
+
const authFailure_js_1 = require("./lib/authFailure.js");
|
|
21
22
|
const logger_js_1 = require("./lib/logger.js");
|
|
22
23
|
const stores_js_1 = require("./lib/stores.js");
|
|
23
24
|
const transportConfig_js_1 = require("./lib/transportConfig.js");
|
|
@@ -33,38 +34,6 @@ function getBodyId(body) {
|
|
|
33
34
|
}
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Classify an authentication failure into a human-readable reason so the proxy
|
|
38
|
-
* can report *why* it is exiting (timeout vs bad credentials vs network ...).
|
|
39
|
-
*/
|
|
40
|
-
function classifyAuthFailure(error) {
|
|
41
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
42
|
-
if (/timeout/i.test(msg)) {
|
|
43
|
-
return {
|
|
44
|
-
category: 'timeout',
|
|
45
|
-
reason: 'the login URL was not completed in time',
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
if (/service key|missing required fields/i.test(msg)) {
|
|
49
|
-
return {
|
|
50
|
-
category: 'service-key',
|
|
51
|
-
reason: 'service key missing or incomplete',
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
if (/invalid_client|unauthorized|\b401\b|invalid credentials/i.test(msg)) {
|
|
55
|
-
return {
|
|
56
|
-
category: 'credentials',
|
|
57
|
-
reason: 'invalid credentials — UAA rejected the client',
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network|ENETUNREACH/i.test(msg)) {
|
|
61
|
-
return {
|
|
62
|
-
category: 'network',
|
|
63
|
-
reason: 'network error reaching the authentication server',
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return { category: 'other', reason: msg };
|
|
67
|
-
}
|
|
68
37
|
/**
|
|
69
38
|
* MCP ABAP ADT Proxy Server
|
|
70
39
|
*/
|
|
@@ -564,7 +533,7 @@ Authorization source: ${authSourceObj}`;
|
|
|
564
533
|
if (this.authExiting)
|
|
565
534
|
return;
|
|
566
535
|
this.authExiting = true;
|
|
567
|
-
const { category, reason } = classifyAuthFailure(error);
|
|
536
|
+
const { category, reason } = (0, authFailure_js_1.classifyAuthFailure)(error);
|
|
568
537
|
const detail = error instanceof Error ? error.message : String(error);
|
|
569
538
|
logger_js_1.logger?.error('Authentication not completed — exiting', {
|
|
570
539
|
type: 'AUTH_FATAL_EXIT',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication failures: the one the proxy raises itself, and how any of
|
|
3
|
+
* them is told apart for the message printed before the standalone proxy
|
|
4
|
+
* exits.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* No UAA credentials for a destination in either store.
|
|
8
|
+
*
|
|
9
|
+
* The proxy's own error, thrown before the broker is asked: auth-broker 3
|
|
10
|
+
* hands a provider's failure back unchanged and reports a missing service key
|
|
11
|
+
* only as a missing `serviceUrl`, so matching its message for this case — as
|
|
12
|
+
* the proxy used to — finds nothing any more. Not retried: a key that is not
|
|
13
|
+
* there now will not be there in a second.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ServiceKeyNotFoundError extends Error {
|
|
16
|
+
readonly destination: string;
|
|
17
|
+
readonly searched: string;
|
|
18
|
+
readonly code = "SERVICE_KEY_NOT_FOUND";
|
|
19
|
+
constructor(destination: string, searched?: string);
|
|
20
|
+
}
|
|
21
|
+
export type AuthFailureCategory = 'timeout' | 'credentials' | 'service-key' | 'network' | 'other';
|
|
22
|
+
/**
|
|
23
|
+
* Classify an authentication failure into a human-readable reason so the proxy
|
|
24
|
+
* can report *why* it is exiting (timeout vs bad credentials vs network ...).
|
|
25
|
+
*
|
|
26
|
+
* By type and `code` first, through the whole `cause` chain: auth-broker 3
|
|
27
|
+
* passes provider errors on unchanged, so their classes and codes arrive
|
|
28
|
+
* intact. The message is read only where nothing typed says more — a browser
|
|
29
|
+
* login's timeout and a UAA refusal are both a plain message inside their
|
|
30
|
+
* error.
|
|
31
|
+
*/
|
|
32
|
+
export declare function classifyAuthFailure(error: unknown): {
|
|
33
|
+
category: AuthFailureCategory;
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=authFailure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authFailure.d.ts","sourceRoot":"","sources":["../../src/lib/authFailure.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH;;;;;;;;GAQG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAI9C,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAJ3B,QAAQ,CAAC,IAAI,2BAA2B;gBAG7B,WAAW,EAAE,MAAM,EACnB,QAAQ,GAAE,MAA4C;CAYlE;AAED,MAAM,MAAM,mBAAmB,GAC3B,SAAS,GACT,aAAa,GACb,aAAa,GACb,SAAS,GACT,OAAO,CAAC;AA8BZ;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG;IACnD,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;CAChB,CA8CA"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Authentication failures: the one the proxy raises itself, and how any of
|
|
4
|
+
* them is told apart for the message printed before the standalone proxy
|
|
5
|
+
* exits.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ServiceKeyNotFoundError = void 0;
|
|
9
|
+
exports.classifyAuthFailure = classifyAuthFailure;
|
|
10
|
+
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
11
|
+
const stores_js_1 = require("./stores.js");
|
|
12
|
+
/**
|
|
13
|
+
* No UAA credentials for a destination in either store.
|
|
14
|
+
*
|
|
15
|
+
* The proxy's own error, thrown before the broker is asked: auth-broker 3
|
|
16
|
+
* hands a provider's failure back unchanged and reports a missing service key
|
|
17
|
+
* only as a missing `serviceUrl`, so matching its message for this case — as
|
|
18
|
+
* the proxy used to — finds nothing any more. Not retried: a key that is not
|
|
19
|
+
* there now will not be there in a second.
|
|
20
|
+
*/
|
|
21
|
+
class ServiceKeyNotFoundError extends Error {
|
|
22
|
+
destination;
|
|
23
|
+
searched;
|
|
24
|
+
code = 'SERVICE_KEY_NOT_FOUND';
|
|
25
|
+
constructor(destination, searched = (0, stores_js_1.getPlatformPaths)('service-keys')[0]) {
|
|
26
|
+
super([
|
|
27
|
+
`Service key file not found for destination "${destination}".`,
|
|
28
|
+
`Please create service key file: ${destination}.json`,
|
|
29
|
+
'Searched in:',
|
|
30
|
+
` - ${searched}`,
|
|
31
|
+
].join('\n'));
|
|
32
|
+
this.destination = destination;
|
|
33
|
+
this.searched = searched;
|
|
34
|
+
this.name = 'ServiceKeyNotFoundError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.ServiceKeyNotFoundError = ServiceKeyNotFoundError;
|
|
38
|
+
const NETWORK_CODES = new Set([
|
|
39
|
+
'ENOTFOUND',
|
|
40
|
+
'ECONNREFUSED',
|
|
41
|
+
'ECONNRESET',
|
|
42
|
+
'ETIMEDOUT',
|
|
43
|
+
'ENETUNREACH',
|
|
44
|
+
'EAI_AGAIN',
|
|
45
|
+
]);
|
|
46
|
+
/** The error and every `cause` behind it, nearest first. */
|
|
47
|
+
function causeChain(error) {
|
|
48
|
+
const chain = [];
|
|
49
|
+
let current = error;
|
|
50
|
+
while (current !== undefined && current !== null && chain.length < 10) {
|
|
51
|
+
chain.push(current);
|
|
52
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
53
|
+
}
|
|
54
|
+
return chain;
|
|
55
|
+
}
|
|
56
|
+
function codeOf(error) {
|
|
57
|
+
if (error !== null && typeof error === 'object' && 'code' in error) {
|
|
58
|
+
const { code } = error;
|
|
59
|
+
return typeof code === 'string' ? code : undefined;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Classify an authentication failure into a human-readable reason so the proxy
|
|
65
|
+
* can report *why* it is exiting (timeout vs bad credentials vs network ...).
|
|
66
|
+
*
|
|
67
|
+
* By type and `code` first, through the whole `cause` chain: auth-broker 3
|
|
68
|
+
* passes provider errors on unchanged, so their classes and codes arrive
|
|
69
|
+
* intact. The message is read only where nothing typed says more — a browser
|
|
70
|
+
* login's timeout and a UAA refusal are both a plain message inside their
|
|
71
|
+
* error.
|
|
72
|
+
*/
|
|
73
|
+
function classifyAuthFailure(error) {
|
|
74
|
+
const chain = causeChain(error);
|
|
75
|
+
if (chain.some((e) => e instanceof ServiceKeyNotFoundError || e instanceof auth_providers_1.ValidationError)) {
|
|
76
|
+
return {
|
|
77
|
+
category: 'service-key',
|
|
78
|
+
reason: 'service key missing or incomplete',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (chain.some((e) => NETWORK_CODES.has(codeOf(e) ?? ''))) {
|
|
82
|
+
return {
|
|
83
|
+
category: 'network',
|
|
84
|
+
reason: 'network error reaching the authentication server',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
88
|
+
if (/timeout|timed out/i.test(msg)) {
|
|
89
|
+
return {
|
|
90
|
+
category: 'timeout',
|
|
91
|
+
reason: 'the login URL was not completed in time',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (/service key|missing required fields/i.test(msg)) {
|
|
95
|
+
return {
|
|
96
|
+
category: 'service-key',
|
|
97
|
+
reason: 'service key missing or incomplete',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (/invalid_client|unauthorized|\b401\b|invalid credentials/i.test(msg)) {
|
|
101
|
+
return {
|
|
102
|
+
category: 'credentials',
|
|
103
|
+
reason: 'invalid credentials — UAA rejected the client',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network|ENETUNREACH/i.test(msg)) {
|
|
107
|
+
return {
|
|
108
|
+
category: 'network',
|
|
109
|
+
reason: 'network error reaching the authentication server',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { category: 'other', reason: msg };
|
|
113
|
+
}
|
package/dist/mcp/supervisor.d.ts
CHANGED
|
@@ -35,6 +35,15 @@ export interface StartOptions {
|
|
|
35
35
|
idleTimeoutMs?: number;
|
|
36
36
|
/** How long a stop waits for requests in flight. See DEFAULT_STOP_GRACE_MS. */
|
|
37
37
|
stopGraceMs?: number;
|
|
38
|
+
/**
|
|
39
|
+
* What to bind, ahead of the config's `httpHost` and the loopback default.
|
|
40
|
+
*
|
|
41
|
+
* Loopback is a default, not a boundary: anything with a shell can forward a
|
|
42
|
+
* port, so refusing to bind elsewhere stopped only the honest case. What the
|
|
43
|
+
* default still does is keep a proxy nobody asked to publish from becoming
|
|
44
|
+
* reachable without someone saying so.
|
|
45
|
+
*/
|
|
46
|
+
host?: string;
|
|
38
47
|
}
|
|
39
48
|
export interface StartedInstance {
|
|
40
49
|
instanceId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../src/mcp/supervisor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAY,KAAK,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEhF,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,QAAiB,CAAC;AAEtD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB,kCAAkC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../src/mcp/supervisor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAY,KAAK,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEhF,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,QAAiB,CAAC;AAEtD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB,kCAAkC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAoBD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,yEAAyE;IACzE,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,eAAe;IAKd,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4B;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmB;IAC5C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAED,OAAO,EAAE,iBAAiB;IAKjD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IA2G5D,yDAAyD;IACnD,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAiD3D,oCAAoC;IACpC,IAAI,IAAI,eAAe,EAAE;IAIzB;;;;;;;;OAQG;IACH,MAAM,IAAI,cAAc,EAAE;IAI1B;;;;OAIG;IACH,OAAO,CAAC,OAAO;IAkBf;;;;;;;OAOG;YACW,aAAa;IA4B3B,OAAO,CAAC,QAAQ;IAahB;;;OAGG;IACH,OAAO,CAAC,OAAO;IAsBf;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAUtB,wDAAwD;IACxD,OAAO,CAAC,eAAe;CAQxB"}
|
package/dist/mcp/supervisor.js
CHANGED
|
@@ -98,9 +98,11 @@ class ProxySupervisor {
|
|
|
98
98
|
// filesystem used to reject `start()` while leaving the listener bound, the
|
|
99
99
|
// credential held, the instance in `owned` and no idle timer to ever reach
|
|
100
100
|
// it — a proxy running that nobody had been told about.
|
|
101
|
+
// Most specific first: this call, then the config, then the default.
|
|
102
|
+
const host = options.host ?? options.config.httpHost ?? this.host;
|
|
101
103
|
let port;
|
|
102
104
|
try {
|
|
103
|
-
port = await (0, ports_js_1.listenOnFreePort)(server,
|
|
105
|
+
port = await (0, ports_js_1.listenOnFreePort)(server, host);
|
|
104
106
|
}
|
|
105
107
|
catch (error) {
|
|
106
108
|
this.release(facade, server, 'listen failed');
|
|
@@ -110,7 +112,7 @@ class ProxySupervisor {
|
|
|
110
112
|
instanceId,
|
|
111
113
|
name: options.name,
|
|
112
114
|
port,
|
|
113
|
-
url: `http://${
|
|
115
|
+
url: `http://${host}:${port}`,
|
|
114
116
|
destination: options.config.btpDestination ?? '(none)',
|
|
115
117
|
startedAt: new Date().toISOString(),
|
|
116
118
|
server,
|
package/dist/mcp/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAaxB,OAAO,EAA2B,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEhF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,QAGc,CAAC;AAE7C,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC;IAC3B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;CACjE;AAMD,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,eAAe,EAC3B,SAAS,GAAE,MAAyB,EACpC,MAAM,GAAE,MAAyB,GAChC,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAaxB,OAAO,EAA2B,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEhF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,QAGc,CAAC;AAE7C,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC;IAC3B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;CACjE;AAMD,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,eAAe,EAC3B,SAAS,GAAE,MAAyB,EACpC,MAAM,GAAE,MAAyB,GAChC,SAAS,EAAE,CA0Mb"}
|
package/dist/mcp/tools.js
CHANGED
|
@@ -93,6 +93,10 @@ function createProxyTools(supervisor, configDir = (0, configs_js_1.proxyConfigDi
|
|
|
93
93
|
.string()
|
|
94
94
|
.optional()
|
|
95
95
|
.describe('Name of an environment, as proxy_environments lists it (for example "e19"). Supplies the ${VAR} values the config references. Needed when the config names no envFile of its own — proxy_configs marks those.'),
|
|
96
|
+
host: zod_1.z
|
|
97
|
+
.string()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('What to bind. Defaults to 127.0.0.1, or the config\u2019s httpHost when it names one. Use "0.0.0.0" to make the proxy reachable from other machines — say so deliberately, since a started proxy is otherwise local only.'),
|
|
96
100
|
idleTimeoutMs: zod_1.z
|
|
97
101
|
.number()
|
|
98
102
|
.optional()
|
|
@@ -108,6 +112,7 @@ function createProxyTools(supervisor, configDir = (0, configs_js_1.proxyConfigDi
|
|
|
108
112
|
const started = await supervisor.start({
|
|
109
113
|
name,
|
|
110
114
|
config: (0, config_js_1.loadConfig)(file, envFile),
|
|
115
|
+
host: args.host,
|
|
111
116
|
idleTimeoutMs: args.idleTimeoutMs,
|
|
112
117
|
});
|
|
113
118
|
return text([
|
package/dist/proxy/btpProxy.d.ts
CHANGED
|
@@ -4,8 +4,18 @@
|
|
|
4
4
|
* For requests with x-sap-destination, proxies to target MCP server
|
|
5
5
|
* with JWT token from auth-broker (XSUAA/BTP)
|
|
6
6
|
*/
|
|
7
|
-
import { AuthBroker } from '@mcp-abap-adt/auth-broker';
|
|
7
|
+
import { AuthBroker, type TokenProviderFactory } from '@mcp-abap-adt/auth-broker';
|
|
8
8
|
import { type ProxyConfig } from '../lib/config.js';
|
|
9
|
+
/**
|
|
10
|
+
* The provider auth-broker builds per destination: an
|
|
11
|
+
* `AuthorizationCodeProvider` seeded with what the stores hold — the UAA
|
|
12
|
+
* credentials with the stored refresh token, and the stored access token — so
|
|
13
|
+
* a session that is still valid, or can be refreshed, needs no browser.
|
|
14
|
+
*
|
|
15
|
+
* How the login is conducted is the provider's authorization strategy; the
|
|
16
|
+
* broker has no say in it any more (its `browser` argument is gone).
|
|
17
|
+
*/
|
|
18
|
+
export declare function authorizationCodeProviderFactory(config: Partial<ProxyConfig>): TokenProviderFactory;
|
|
9
19
|
/**
|
|
10
20
|
* Check if error messages should be written to stderr
|
|
11
21
|
* Only output in verbose mode and not in test environment
|
|
@@ -32,10 +42,6 @@ export declare class BtpProxy {
|
|
|
32
42
|
* If broker doesn't exist in map, creates new one and stores it
|
|
33
43
|
*/
|
|
34
44
|
private getOrCreateBtpAuthBroker;
|
|
35
|
-
/**
|
|
36
|
-
* Helper to ensure valid serviceUrl in session if override provided
|
|
37
|
-
*/
|
|
38
|
-
private ensureSessionServiceUrl;
|
|
39
45
|
/**
|
|
40
46
|
* The `Authorization` header value for a destination, or `null` when this
|
|
41
47
|
* credential is not a header at all.
|
|
@@ -54,15 +60,6 @@ export declare class BtpProxy {
|
|
|
54
60
|
* them, so it fails on the first attempt instead of three times slower.
|
|
55
61
|
*/
|
|
56
62
|
getAuthorizationHeader(destination: string): Promise<string | null>;
|
|
57
|
-
/**
|
|
58
|
-
* Turn the broker's failure into one that names what this proxy needs.
|
|
59
|
-
*
|
|
60
|
-
* The broker speaks of `.env` and `mcp.env` because it can be fed either way.
|
|
61
|
-
* This proxy only ever reads service keys, so passing that message through
|
|
62
|
-
* told the most common failure — a missing key — to go and create a file that
|
|
63
|
-
* would be ignored.
|
|
64
|
-
*/
|
|
65
|
-
private explainAuthFailure;
|
|
66
63
|
dispose(): void;
|
|
67
64
|
/**
|
|
68
65
|
* Get the target service URL for a destination.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"btpProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/btpProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"btpProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/btpProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,UAAU,EAIV,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAMnC,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA8EhE;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,GAC3B,oBAAoB,CAuBtB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAU3C;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;IACrD,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAU;gBAEZ,oBAAoB,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAsC3E;;;OAGG;IACU,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB3D;;;;OAIG;YACW,wBAAwB;IAmDtC;;;;;;;;;;;;;;;;OAgBG;IACG,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAkClE,OAAO,IAAI,IAAI;IAKtB;;;OAGG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAexD;;OAEG;WACiB,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;CAkB7E;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
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.BtpProxy = void 0;
|
|
10
|
+
exports.authorizationCodeProviderFactory = authorizationCodeProviderFactory;
|
|
10
11
|
exports.shouldWriteStderr = shouldWriteStderr;
|
|
11
12
|
exports.createBtpProxy = createBtpProxy;
|
|
12
13
|
const auth_broker_1 = require("@mcp-abap-adt/auth-broker");
|
|
13
14
|
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
15
|
+
const authFailure_js_1 = require("../lib/authFailure.js");
|
|
14
16
|
const config_js_1 = require("../lib/config.js");
|
|
15
17
|
const logger_js_1 = require("../lib/logger.js");
|
|
16
18
|
const stores_js_1 = require("../lib/stores.js");
|
|
@@ -49,6 +51,52 @@ const loggerAdapter = {
|
|
|
49
51
|
: undefined),
|
|
50
52
|
};
|
|
51
53
|
const errorHandler_js_1 = require("../lib/errorHandler.js");
|
|
54
|
+
const targetUrlSessionStore_js_1 = require("./targetUrlSessionStore.js");
|
|
55
|
+
async function requireCredentials(destination, serviceKeyStore, sessionStore) {
|
|
56
|
+
// A store answers a missing file with null. Anything it throws — a key
|
|
57
|
+
// that is not valid JSON, a file it may not read — is a different problem
|
|
58
|
+
// with a different fix, and reaches the caller as the store raised it,
|
|
59
|
+
// naming the file; answering it as "not found" sent users to create a file
|
|
60
|
+
// that was already there.
|
|
61
|
+
const found = (await serviceKeyStore.getAuthorizationConfig(destination)) ??
|
|
62
|
+
(await sessionStore.getAuthorizationConfig(destination));
|
|
63
|
+
if (!found) {
|
|
64
|
+
throw new authFailure_js_1.ServiceKeyNotFoundError(destination);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The provider auth-broker builds per destination: an
|
|
69
|
+
* `AuthorizationCodeProvider` seeded with what the stores hold — the UAA
|
|
70
|
+
* credentials with the stored refresh token, and the stored access token — so
|
|
71
|
+
* a session that is still valid, or can be refreshed, needs no browser.
|
|
72
|
+
*
|
|
73
|
+
* How the login is conducted is the provider's authorization strategy; the
|
|
74
|
+
* broker has no say in it any more (its `browser` argument is gone).
|
|
75
|
+
*/
|
|
76
|
+
function authorizationCodeProviderFactory(config) {
|
|
77
|
+
return (destination, authConfig, connConfig) => {
|
|
78
|
+
if (!authConfig) {
|
|
79
|
+
throw new authFailure_js_1.ServiceKeyNotFoundError(destination);
|
|
80
|
+
}
|
|
81
|
+
return new auth_providers_1.AuthorizationCodeProvider({
|
|
82
|
+
authorization: (0, auth_providers_1.browserCallbackStrategy)({
|
|
83
|
+
browser: config.browser,
|
|
84
|
+
port: config.browserAuthPort || DEFAULT_BROWSER_AUTH_PORT,
|
|
85
|
+
timeoutMs: INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
86
|
+
}),
|
|
87
|
+
// Pass a logger so the callback strategy can surface the "Open this
|
|
88
|
+
// URL" prompt in 'none'/'headless' mode. Without it, the callback
|
|
89
|
+
// server gets a null logger and silently drops the authorization URL.
|
|
90
|
+
logger: loggerAdapter,
|
|
91
|
+
uaaUrl: authConfig.uaaUrl,
|
|
92
|
+
clientId: authConfig.uaaClientId,
|
|
93
|
+
clientSecret: authConfig.uaaClientSecret,
|
|
94
|
+
refreshToken: authConfig.refreshToken,
|
|
95
|
+
// An empty string is how a session seeded without a token says "none".
|
|
96
|
+
accessToken: connConfig.authorizationToken || undefined,
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
}
|
|
52
100
|
/**
|
|
53
101
|
* Check if error messages should be written to stderr
|
|
54
102
|
* Only output in verbose mode and not in test environment
|
|
@@ -134,15 +182,15 @@ class BtpProxy {
|
|
|
134
182
|
* If destination is not provided, returns default broker
|
|
135
183
|
* If broker doesn't exist in map, creates new one and stores it
|
|
136
184
|
*/
|
|
137
|
-
async getOrCreateBtpAuthBroker(destination
|
|
185
|
+
async getOrCreateBtpAuthBroker(destination) {
|
|
138
186
|
// If no destination, use default broker
|
|
139
187
|
if (!destination) {
|
|
140
188
|
return this.defaultBtpAuthBroker;
|
|
141
189
|
}
|
|
142
190
|
// Check if broker exists in map
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
return
|
|
191
|
+
const existing = this.btpAuthBrokers.get(destination);
|
|
192
|
+
if (existing) {
|
|
193
|
+
return existing;
|
|
146
194
|
}
|
|
147
195
|
// Create new broker for this destination
|
|
148
196
|
logger_js_1.logger?.info('Creating new BTP auth broker for destination', {
|
|
@@ -150,146 +198,24 @@ class BtpProxy {
|
|
|
150
198
|
destination,
|
|
151
199
|
});
|
|
152
200
|
const { serviceKeyStore, sessionStore } = await (0, stores_js_1.getPlatformStores)(this.unsafe);
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
logger_js_1.logger?.warn('Failed to load auth config for provider initialization', {
|
|
167
|
-
error: error instanceof Error ? error.message : String(error),
|
|
168
|
-
destination,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
if (!authConfig) {
|
|
172
|
-
const serviceKeyDir = (0, stores_js_1.getPlatformPaths)('service-keys')[0];
|
|
173
|
-
logger_js_1.logger?.error('Service key not found for destination', {
|
|
174
|
-
destination,
|
|
175
|
-
hint: `Ensure service key file "${destination}.json" exists in ${serviceKeyDir} (override the base dir with AUTH_BROKER_PATH)`,
|
|
176
|
-
});
|
|
177
|
-
// We cannot proceed without config, but we'll let it fail with a clear message
|
|
178
|
-
// Or we could throw here.
|
|
179
|
-
// If we don't provide config, provider will throw "Missing required fields".
|
|
180
|
-
}
|
|
181
|
-
// Always use AuthorizationCodeProvider (enforced)
|
|
182
|
-
// Map IAuthorizationConfig (uaaClientId) to ProviderConfig (clientId)
|
|
183
|
-
// How the login is conducted is now a strategy the provider config
|
|
184
|
-
// carries under `authorization`; the callback port lives inside it.
|
|
185
|
-
//
|
|
186
|
-
// uaaUrl/clientId/clientSecret fall back to '' rather than being omitted
|
|
187
|
-
// when authConfig is missing: AuthorizationCodeProviderConfig declares
|
|
188
|
-
// them required, and the provider's own constructor already treats an
|
|
189
|
-
// empty string the same as absent, throwing its "Missing required
|
|
190
|
-
// fields" ValidationError — which is the clear failure this was always
|
|
191
|
-
// meant to produce.
|
|
192
|
-
const providerConfig = {
|
|
193
|
-
authorization: (0, auth_providers_1.browserCallbackStrategy)({
|
|
194
|
-
browser: this.config.browser,
|
|
195
|
-
port: this.config.browserAuthPort || DEFAULT_BROWSER_AUTH_PORT,
|
|
196
|
-
timeoutMs: INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
197
|
-
}),
|
|
198
|
-
// Pass a logger so the callback strategy can surface the "Open this
|
|
199
|
-
// URL" prompt in 'none'/'headless' mode. Without it, the callback
|
|
200
|
-
// server gets a null logger and silently drops the authorization URL.
|
|
201
|
-
logger: loggerAdapter,
|
|
202
|
-
uaaUrl: authConfig?.uaaUrl ?? '',
|
|
203
|
-
clientId: authConfig?.uaaClientId ?? '',
|
|
204
|
-
clientSecret: authConfig?.uaaClientSecret ?? '',
|
|
205
|
-
};
|
|
206
|
-
const tokenProvider = new auth_providers_1.AuthorizationCodeProvider(providerConfig);
|
|
207
|
-
broker = new auth_broker_1.AuthBroker({
|
|
201
|
+
// auth-broker reports a missing service key only as a missing
|
|
202
|
+
// `serviceUrl` — and not at all once a targetUrl is seeded below — so the
|
|
203
|
+
// proxy asks the stores itself, before anything is written, and fails
|
|
204
|
+
// with its own error naming the file to create.
|
|
205
|
+
await requireCredentials(destination, serviceKeyStore, sessionStore);
|
|
206
|
+
// The broker is told the target URL through the store it reads, not by
|
|
207
|
+
// writing it into the session: that session may be the file mcp-auth
|
|
208
|
+
// writes, whose own URL must stay.
|
|
209
|
+
const targetUrl = this.config.targetUrl;
|
|
210
|
+
const brokerSessionStore = targetUrl
|
|
211
|
+
? new targetUrlSessionStore_js_1.TargetUrlSessionStore(sessionStore, targetUrl)
|
|
212
|
+
: sessionStore;
|
|
213
|
+
const broker = new auth_broker_1.AuthBroker({
|
|
208
214
|
serviceKeyStore,
|
|
209
|
-
sessionStore,
|
|
210
|
-
|
|
211
|
-
},
|
|
212
|
-
logger_js_1.logger);
|
|
215
|
+
sessionStore: brokerSessionStore,
|
|
216
|
+
provider: authorizationCodeProviderFactory(this.config),
|
|
217
|
+
}, loggerAdapter);
|
|
213
218
|
this.btpAuthBrokers.set(destination, broker);
|
|
214
|
-
return this.ensureSessionServiceUrl(broker, destination, targetUrl);
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Helper to ensure valid serviceUrl in session if override provided
|
|
218
|
-
*/
|
|
219
|
-
async ensureSessionServiceUrl(broker, destination, targetUrl) {
|
|
220
|
-
const activeTargetUrl = targetUrl || this.config.targetUrl;
|
|
221
|
-
if (!activeTargetUrl) {
|
|
222
|
-
return broker;
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
// Check if current connection config exists
|
|
226
|
-
const currentConn = await broker.getConnectionConfig(destination);
|
|
227
|
-
// We need to ensure we have a valid session with BOTH serviceUrl AND auth config.
|
|
228
|
-
// Even if serviceUrl matches, the auth config might be missing from the session
|
|
229
|
-
// (which causes ClientCredentialsProvider to fail if initialized with empty config).
|
|
230
|
-
// Cast to any to access potentially private methods if interface restricted
|
|
231
|
-
// biome-ignore lint/suspicious/noExplicitAny: Accessing internal methods for safe injection
|
|
232
|
-
const brokerAny = broker;
|
|
233
|
-
let authConfig = await broker.getAuthorizationConfig(destination);
|
|
234
|
-
if (!authConfig) {
|
|
235
|
-
try {
|
|
236
|
-
if (typeof brokerAny.getAuthorizationConfigFromServiceKey === 'function') {
|
|
237
|
-
authConfig =
|
|
238
|
-
await brokerAny.getAuthorizationConfigFromServiceKey(destination);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
catch (e) {
|
|
242
|
-
logger_js_1.logger?.debug('Could not find auth config for session update', {
|
|
243
|
-
error: String(e),
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (!authConfig) {
|
|
248
|
-
authConfig = {
|
|
249
|
-
uaaUrl: 'https://placeholder.authentication.sap.hana.ondemand.com',
|
|
250
|
-
uaaClientId: 'placeholder',
|
|
251
|
-
uaaClientSecret: 'placeholder',
|
|
252
|
-
};
|
|
253
|
-
logger_js_1.logger?.info('Using placeholder auth config for session injection', {
|
|
254
|
-
type: 'BTP_SESSION_PLACEHOLDER',
|
|
255
|
-
destination,
|
|
256
|
-
targetUrl: activeTargetUrl,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
if (authConfig) {
|
|
260
|
-
const newConn = {
|
|
261
|
-
...(currentConn || {}),
|
|
262
|
-
serviceUrl: activeTargetUrl,
|
|
263
|
-
authType: 'jwt',
|
|
264
|
-
// Map XSUAA keys to ClientCredentialsProvider keys
|
|
265
|
-
clientId: authConfig.uaaClientId,
|
|
266
|
-
clientSecret: authConfig.uaaClientSecret,
|
|
267
|
-
uaaUrl: authConfig.uaaUrl,
|
|
268
|
-
};
|
|
269
|
-
if (typeof brokerAny.saveTokenToSession === 'function') {
|
|
270
|
-
await brokerAny.saveTokenToSession(destination, newConn, authConfig);
|
|
271
|
-
logger_js_1.logger?.info('Injected targetUrl and auth config into BTP session', {
|
|
272
|
-
type: 'BTP_SESSION_INJECTION',
|
|
273
|
-
destination,
|
|
274
|
-
url: activeTargetUrl,
|
|
275
|
-
hasClientId: !!newConn.clientId,
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
logger_js_1.logger?.error('saveTokenToSession is not a function on broker', {
|
|
280
|
-
type: 'BTP_SESSION_METHOD_MISSING',
|
|
281
|
-
keys: Object.keys(brokerAny),
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
catch (error) {
|
|
287
|
-
logger_js_1.logger?.error('Failed to inject targetUrl into session', {
|
|
288
|
-
type: 'BTP_SESSION_INJECTION_ERROR',
|
|
289
|
-
destination,
|
|
290
|
-
error: error instanceof Error ? error.message : String(error),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
219
|
return broker;
|
|
294
220
|
}
|
|
295
221
|
/**
|
|
@@ -326,7 +252,10 @@ class BtpProxy {
|
|
|
326
252
|
}, retryOptions);
|
|
327
253
|
}
|
|
328
254
|
catch (error) {
|
|
329
|
-
|
|
255
|
+
// Passed on as it stands: auth-broker 3 no longer rewords a provider's
|
|
256
|
+
// failure, and a missing service key is already this proxy's own
|
|
257
|
+
// ServiceKeyNotFoundError, which names the file to create.
|
|
258
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
330
259
|
logger_js_1.logger?.error('Failed to get an authorization header', {
|
|
331
260
|
type: 'CREDENTIAL_HEADER_ERROR',
|
|
332
261
|
destination,
|
|
@@ -335,36 +264,8 @@ class BtpProxy {
|
|
|
335
264
|
if (shouldWriteStderr()) {
|
|
336
265
|
process.stderr.write(`[MCP Proxy] ✗ ${message}\n`);
|
|
337
266
|
}
|
|
338
|
-
throw
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Turn the broker's failure into one that names what this proxy needs.
|
|
343
|
-
*
|
|
344
|
-
* The broker speaks of `.env` and `mcp.env` because it can be fed either way.
|
|
345
|
-
* This proxy only ever reads service keys, so passing that message through
|
|
346
|
-
* told the most common failure — a missing key — to go and create a file that
|
|
347
|
-
* would be ignored.
|
|
348
|
-
*/
|
|
349
|
-
explainAuthFailure(error, destination) {
|
|
350
|
-
const original = error instanceof Error ? error.message : String(error);
|
|
351
|
-
if (!original.includes('.env') && !original.includes('mcp.env')) {
|
|
352
|
-
return original;
|
|
267
|
+
throw error;
|
|
353
268
|
}
|
|
354
|
-
const searched = original.match(/Searched in:\s*([\s\S]*?)(?:\n\n|$)/);
|
|
355
|
-
const paths = searched
|
|
356
|
-
? searched[1]
|
|
357
|
-
.trim()
|
|
358
|
-
.split('\n')
|
|
359
|
-
.map((p) => p.trim().replace(/^-\s*/, ''))
|
|
360
|
-
.filter(Boolean)
|
|
361
|
-
: [(0, stores_js_1.storeDir)('service-keys')];
|
|
362
|
-
return [
|
|
363
|
-
`Service key file not found for destination "${destination}".`,
|
|
364
|
-
`Please create service key file: ${destination}.json`,
|
|
365
|
-
'Searched in:',
|
|
366
|
-
...paths.map((p) => ` - ${p}`),
|
|
367
|
-
].join('\n');
|
|
368
269
|
}
|
|
369
270
|
dispose() {
|
|
370
271
|
this.credentials.clear();
|
|
@@ -396,28 +297,8 @@ class BtpProxy {
|
|
|
396
297
|
const defaultBtpAuthBroker = new auth_broker_1.AuthBroker({
|
|
397
298
|
serviceKeyStore,
|
|
398
299
|
sessionStore,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
clientId: 'placeholder',
|
|
402
|
-
clientSecret: 'placeholder',
|
|
403
|
-
authorization: (0, auth_providers_1.browserCallbackStrategy)({
|
|
404
|
-
browser: loadedConfig.browser,
|
|
405
|
-
// This call site never had its own fallback: it passed
|
|
406
|
-
// `redirectPort` straight through and relied on
|
|
407
|
-
// auth-providers@1.2.0 defaulting an omitted value to 3001
|
|
408
|
-
// internally — which is this proxy's own default `httpPort`
|
|
409
|
-
// (src/lib/config.ts). An omitted `browserAuthPort` therefore
|
|
410
|
-
// told the callback server to bind the port the proxy already
|
|
411
|
-
// listens on, failing every such login with "Port 3001 is
|
|
412
|
-
// already in use". Both call sites now share the one documented
|
|
413
|
-
// default instead of leaving this one to an invisible, colliding
|
|
414
|
-
// number.
|
|
415
|
-
port: loadedConfig.browserAuthPort || DEFAULT_BROWSER_AUTH_PORT,
|
|
416
|
-
timeoutMs: INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
417
|
-
}),
|
|
418
|
-
logger: loggerAdapter,
|
|
419
|
-
}),
|
|
420
|
-
}, 'none', loggerAdapter);
|
|
300
|
+
provider: authorizationCodeProviderFactory(loadedConfig),
|
|
301
|
+
}, loggerAdapter);
|
|
421
302
|
return new BtpProxy(defaultBtpAuthBroker, loadedConfig);
|
|
422
303
|
}
|
|
423
304
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session store that tells the broker the proxy's `targetUrl` without
|
|
3
|
+
* writing it over the destination's own URL.
|
|
4
|
+
*
|
|
5
|
+
* auth-broker 3 refuses a destination whose session and service key both lack
|
|
6
|
+
* a `serviceUrl`, and an XSUAA service key for a BTP-hosted MCP server usually
|
|
7
|
+
* carries none: the target URL is what the proxy is told instead. The proxy
|
|
8
|
+
* used to write it into the session before building the broker — and, with
|
|
9
|
+
* `unsafe`, that session is the file `mcp-auth` writes too, so its `SAP_URL`
|
|
10
|
+
* was replaced by the MCP server's URL.
|
|
11
|
+
*
|
|
12
|
+
* Reads answer the target URL as the `serviceUrl`. Writes keep the URL the
|
|
13
|
+
* session already holds; only a session that does not exist yet is created
|
|
14
|
+
* with the target URL, since a store cannot create one without a URL.
|
|
15
|
+
* Everything else passes through to the store it wraps.
|
|
16
|
+
*/
|
|
17
|
+
import type { IAuthorizationConfig, IConfig, IConnectionConfig, ISessionStore } from '@mcp-abap-adt/interfaces-auth-sap';
|
|
18
|
+
export declare class TargetUrlSessionStore implements ISessionStore {
|
|
19
|
+
private readonly inner;
|
|
20
|
+
private readonly targetUrl;
|
|
21
|
+
constructor(inner: ISessionStore, targetUrl: string);
|
|
22
|
+
loadSession(destination: string): Promise<IConfig | null>;
|
|
23
|
+
getConnectionConfig(destination: string): Promise<IConnectionConfig | null>;
|
|
24
|
+
getAuthorizationConfig(destination: string): Promise<IAuthorizationConfig | null>;
|
|
25
|
+
setAuthorizationConfig(destination: string, config: IAuthorizationConfig): Promise<void>;
|
|
26
|
+
setConnectionConfig(destination: string, config: IConnectionConfig): Promise<void>;
|
|
27
|
+
saveSession(destination: string, config: IConfig | unknown): Promise<void>;
|
|
28
|
+
deleteSession(destination: string): Promise<void>;
|
|
29
|
+
/** The URL the session holds, or the target URL for a session not yet written. */
|
|
30
|
+
private ownServiceUrl;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=targetUrlSessionStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targetUrlSessionStore.d.ts","sourceRoot":"","sources":["../../src/proxy/targetUrlSessionStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EACV,oBAAoB,EACpB,OAAO,EACP,iBAAiB,EACjB,aAAa,EACd,MAAM,mCAAmC,CAAC;AAE3C,qBAAa,qBAAsB,YAAW,aAAa;IAEvD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,KAAK,EAAE,aAAa,EACpB,SAAS,EAAE,MAAM;IAG9B,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAKzD,mBAAmB,CACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAKpC,sBAAsB,CACpB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAIvC,sBAAsB,CACpB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IAIV,mBAAmB,CACvB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,IAAI,CAAC;IAOV,WAAW,CACf,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,OAAO,GAAG,OAAO,GACxB,OAAO,CAAC,IAAI,CAAC;IAOV,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvD,kFAAkF;YACpE,aAAa;CAI5B"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A session store that tells the broker the proxy's `targetUrl` without
|
|
4
|
+
* writing it over the destination's own URL.
|
|
5
|
+
*
|
|
6
|
+
* auth-broker 3 refuses a destination whose session and service key both lack
|
|
7
|
+
* a `serviceUrl`, and an XSUAA service key for a BTP-hosted MCP server usually
|
|
8
|
+
* carries none: the target URL is what the proxy is told instead. The proxy
|
|
9
|
+
* used to write it into the session before building the broker — and, with
|
|
10
|
+
* `unsafe`, that session is the file `mcp-auth` writes too, so its `SAP_URL`
|
|
11
|
+
* was replaced by the MCP server's URL.
|
|
12
|
+
*
|
|
13
|
+
* Reads answer the target URL as the `serviceUrl`. Writes keep the URL the
|
|
14
|
+
* session already holds; only a session that does not exist yet is created
|
|
15
|
+
* with the target URL, since a store cannot create one without a URL.
|
|
16
|
+
* Everything else passes through to the store it wraps.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.TargetUrlSessionStore = void 0;
|
|
20
|
+
class TargetUrlSessionStore {
|
|
21
|
+
inner;
|
|
22
|
+
targetUrl;
|
|
23
|
+
constructor(inner, targetUrl) {
|
|
24
|
+
this.inner = inner;
|
|
25
|
+
this.targetUrl = targetUrl;
|
|
26
|
+
}
|
|
27
|
+
async loadSession(destination) {
|
|
28
|
+
const session = await this.inner.loadSession(destination);
|
|
29
|
+
return session ? { ...session, serviceUrl: this.targetUrl } : null;
|
|
30
|
+
}
|
|
31
|
+
async getConnectionConfig(destination) {
|
|
32
|
+
const connection = await this.inner.getConnectionConfig(destination);
|
|
33
|
+
return { ...(connection ?? {}), serviceUrl: this.targetUrl };
|
|
34
|
+
}
|
|
35
|
+
getAuthorizationConfig(destination) {
|
|
36
|
+
return this.inner.getAuthorizationConfig(destination);
|
|
37
|
+
}
|
|
38
|
+
setAuthorizationConfig(destination, config) {
|
|
39
|
+
return this.inner.setAuthorizationConfig(destination, config);
|
|
40
|
+
}
|
|
41
|
+
async setConnectionConfig(destination, config) {
|
|
42
|
+
await this.inner.setConnectionConfig(destination, {
|
|
43
|
+
...config,
|
|
44
|
+
serviceUrl: await this.ownServiceUrl(destination),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async saveSession(destination, config) {
|
|
48
|
+
await this.inner.saveSession(destination, {
|
|
49
|
+
...config,
|
|
50
|
+
serviceUrl: await this.ownServiceUrl(destination),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async deleteSession(destination) {
|
|
54
|
+
await this.inner.deleteSession?.(destination);
|
|
55
|
+
}
|
|
56
|
+
/** The URL the session holds, or the target URL for a session not yet written. */
|
|
57
|
+
async ownServiceUrl(destination) {
|
|
58
|
+
const stored = await this.inner.getConnectionConfig(destination);
|
|
59
|
+
return stored?.serviceUrl ?? this.targetUrl;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.TargetUrlSessionStore = TargetUrlSessionStore;
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -226,7 +226,13 @@ from `mcp-abap-adt-proxy` in exactly two ways, both deliberate:
|
|
|
226
226
|
- **The port in the config is ignored.** A free one is bound instead and
|
|
227
227
|
`proxy_start` reports the URL it got. Four of eight configs in practice declare
|
|
228
228
|
`httpPort: 3001`, so honouring it is what made running two of them impossible.
|
|
229
|
-
- **`httpHost` is
|
|
229
|
+
- **`httpHost` is honoured**, and `proxy_start` takes a `host` of its own that
|
|
230
|
+
wins over it. The default when neither says anything is `127.0.0.1`.
|
|
231
|
+
|
|
232
|
+
That default is a default, not a boundary: anything with a shell can forward a
|
|
233
|
+
port, so refusing to bind elsewhere only stopped the honest case. What it still
|
|
234
|
+
does is keep a proxy nobody asked to publish from becoming reachable without
|
|
235
|
+
someone saying so.
|
|
230
236
|
|
|
231
237
|
Everything else — destination, `targetUrl`, `defaultHeaders`, `browser`,
|
|
232
238
|
`browserAuthPort`, timeouts, `envFile` interpolation — comes from the config as
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -171,16 +171,30 @@ ping cloud-llm-hub.example.com
|
|
|
171
171
|
better; each attempt logs `RETRY_ATTEMPT`, and the final failure logs
|
|
172
172
|
`CREDENTIAL_HEADER_ERROR` with the reason.
|
|
173
173
|
|
|
174
|
-
#### Error: "
|
|
174
|
+
#### Error: "Service key file not found for destination …" (or another token failure)
|
|
175
175
|
|
|
176
176
|
**Symptoms:**
|
|
177
|
-
- Proxy requests fail
|
|
178
|
-
-
|
|
177
|
+
- Proxy requests fail, or the standalone proxy exits at startup
|
|
178
|
+
- `CREDENTIAL_HEADER_ERROR` in the log
|
|
179
179
|
|
|
180
180
|
**Causes:**
|
|
181
|
-
- Service key not found
|
|
182
|
-
-
|
|
183
|
-
|
|
181
|
+
- Service key not found. The proxy checks for it itself before asking the
|
|
182
|
+
auth-broker, and says so in these words:
|
|
183
|
+
```
|
|
184
|
+
Service key file not found for destination "sk".
|
|
185
|
+
Please create service key file: sk.json
|
|
186
|
+
Searched in:
|
|
187
|
+
- /home/you/.config/mcp-abap-adt/service-keys
|
|
188
|
+
```
|
|
189
|
+
- Invalid or incomplete service key — auth-providers' `ValidationError`, naming
|
|
190
|
+
the missing fields
|
|
191
|
+
- A browser login that did not complete — auth-providers' `BrowserAuthError`
|
|
192
|
+
(timeout, the identity provider's refusal, a busy callback port, a browser
|
|
193
|
+
that would not open)
|
|
194
|
+
|
|
195
|
+
Since the move to auth-broker 3, the provider's own error reaches the log as
|
|
196
|
+
it was raised; the broker no longer rewraps it into
|
|
197
|
+
`Token provider … error for <destination>`.
|
|
184
198
|
|
|
185
199
|
**Solutions:**
|
|
186
200
|
|
|
@@ -215,9 +229,9 @@ export AUTH_BROKER_PATH="/custom/path"
|
|
|
215
229
|
# Sessions will be resolved from /custom/path/sessions
|
|
216
230
|
```
|
|
217
231
|
|
|
218
|
-
4. **Test authentication manually
|
|
232
|
+
4. **Test authentication manually** — through the proxy's own path:
|
|
219
233
|
```bash
|
|
220
|
-
|
|
234
|
+
npm run test-destination -- sk
|
|
221
235
|
```
|
|
222
236
|
|
|
223
237
|
### 3. Routing Issues
|
|
@@ -359,16 +373,16 @@ export LOG_LEVEL=debug
|
|
|
359
373
|
|
|
360
374
|
**Solutions:**
|
|
361
375
|
|
|
362
|
-
1. **Token
|
|
363
|
-
-
|
|
364
|
-
|
|
365
|
-
-
|
|
376
|
+
1. **Token renewal is the credential's, not the proxy's:**
|
|
377
|
+
- The proxy keeps no token cache; it asks the destination's credential for
|
|
378
|
+
a header on every request
|
|
379
|
+
- The auth-broker's provider renews an expired token with the refresh token,
|
|
380
|
+
or runs the browser login when there is none or it is refused
|
|
381
|
+
- A 401 from the target is passed back to the client; the proxy does not
|
|
382
|
+
retry the request
|
|
366
383
|
|
|
367
|
-
2. **
|
|
368
|
-
|
|
369
|
-
# Tokens are cached for 30 minutes
|
|
370
|
-
# On 401/403 the proxy automatically clears the cache and retries with a fresh token
|
|
371
|
-
```
|
|
384
|
+
2. **With `--unsafe`**, the session file holds the service URL, the token and
|
|
385
|
+
the refresh token — not the client secret, which stays in the service key.
|
|
372
386
|
|
|
373
387
|
3. **Verify service key:**
|
|
374
388
|
```bash
|
|
@@ -438,8 +452,8 @@ Look for logs like:
|
|
|
438
452
|
|
|
439
453
|
Look for logs like:
|
|
440
454
|
```
|
|
441
|
-
[DEBUG]
|
|
442
|
-
[
|
|
455
|
+
[DEBUG] Asking the credential for a header
|
|
456
|
+
[INFO] [AuthBroker] Token saved for sk
|
|
443
457
|
```
|
|
444
458
|
|
|
445
459
|
## Getting Help
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/proxy",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.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",
|
|
@@ -63,13 +63,13 @@
|
|
|
63
63
|
"typescript": "^6.0.3"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@mcp-abap-adt/auth-broker": "^
|
|
67
|
-
"@mcp-abap-adt/auth-providers": "^
|
|
68
|
-
"@mcp-abap-adt/auth-stores": "^1.2.
|
|
69
|
-
"@mcp-abap-adt/connection": "^9.
|
|
70
|
-
"@mcp-abap-adt/header-validator": "^0.3.
|
|
71
|
-
"@mcp-abap-adt/interfaces-auth": "^1.
|
|
72
|
-
"@mcp-abap-adt/interfaces-auth-sap": "^1.0.
|
|
66
|
+
"@mcp-abap-adt/auth-broker": "^3.0.0",
|
|
67
|
+
"@mcp-abap-adt/auth-providers": "^4.2.0",
|
|
68
|
+
"@mcp-abap-adt/auth-stores": "^1.2.3",
|
|
69
|
+
"@mcp-abap-adt/connection": "^9.3.0",
|
|
70
|
+
"@mcp-abap-adt/header-validator": "^0.3.1",
|
|
71
|
+
"@mcp-abap-adt/interfaces-auth": "^2.1.0",
|
|
72
|
+
"@mcp-abap-adt/interfaces-auth-sap": "^1.0.1",
|
|
73
73
|
"@mcp-abap-adt/interfaces-network": "^2.0.0",
|
|
74
74
|
"@mcp-abap-adt/logger": "^0.4.0",
|
|
75
75
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"zod": "^4.3.6"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
|
-
"node": "
|
|
82
|
+
"node": "^22 || ^24",
|
|
83
83
|
"npm": ">=9.0.0"
|
|
84
84
|
},
|
|
85
85
|
"files": [
|