@mcp-abap-adt/proxy 2.0.0 → 4.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +669 -17
  3. package/README.md +79 -8
  4. package/bin/mcp-abap-adt-proxy-mcp.js +113 -0
  5. package/dist/index.d.ts +10 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +54 -97
  8. package/dist/lib/stores.d.ts +23 -2
  9. package/dist/lib/stores.d.ts.map +1 -1
  10. package/dist/lib/stores.js +56 -1
  11. package/dist/mcp/cli.d.ts +2 -0
  12. package/dist/mcp/cli.d.ts.map +1 -0
  13. package/dist/mcp/cli.js +21 -0
  14. package/dist/mcp/configs.d.ts +33 -0
  15. package/dist/mcp/configs.d.ts.map +1 -0
  16. package/dist/mcp/configs.js +142 -0
  17. package/dist/mcp/ports.d.ts +15 -0
  18. package/dist/mcp/ports.d.ts.map +1 -0
  19. package/dist/mcp/ports.js +35 -0
  20. package/dist/mcp/registry.d.ts +57 -0
  21. package/dist/mcp/registry.d.ts.map +1 -0
  22. package/dist/mcp/registry.js +145 -0
  23. package/dist/mcp/server.d.ts +34 -0
  24. package/dist/mcp/server.d.ts.map +1 -0
  25. package/dist/mcp/server.js +79 -0
  26. package/dist/mcp/shutdown.d.ts +37 -0
  27. package/dist/mcp/shutdown.d.ts.map +1 -0
  28. package/dist/mcp/shutdown.js +82 -0
  29. package/dist/mcp/supervisor.d.ts +125 -0
  30. package/dist/mcp/supervisor.d.ts.map +1 -0
  31. package/dist/mcp/supervisor.js +330 -0
  32. package/dist/mcp/tools.d.ts +28 -0
  33. package/dist/mcp/tools.d.ts.map +1 -0
  34. package/dist/mcp/tools.js +152 -0
  35. package/dist/proxy/btpProxy.d.ts +24 -73
  36. package/dist/proxy/btpProxy.d.ts.map +1 -1
  37. package/dist/proxy/btpProxy.js +65 -616
  38. package/dist/proxy/credentials.d.ts +45 -0
  39. package/dist/proxy/credentials.d.ts.map +1 -0
  40. package/dist/proxy/credentials.js +41 -0
  41. package/dist/proxy/requestHandler.d.ts +38 -0
  42. package/dist/proxy/requestHandler.d.ts.map +1 -0
  43. package/dist/proxy/requestHandler.js +73 -0
  44. package/dist/proxy/reverseProxy.d.ts +11 -2
  45. package/dist/proxy/reverseProxy.d.ts.map +1 -1
  46. package/dist/proxy/reverseProxy.js +52 -9
  47. package/dist/router/headerAnalyzer.js +2 -2
  48. package/dist/router/requestInterceptor.js +9 -9
  49. package/docs/API.md +172 -0
  50. package/docs/ARCHITECTURE.md +322 -0
  51. package/docs/CLIENT_SETUP.md +413 -0
  52. package/docs/CONFIGURATION.md +258 -0
  53. package/docs/MIGRATION-4.0.md +125 -0
  54. package/docs/ROUTING_LOGIC.md +126 -0
  55. package/docs/TROUBLESHOOTING.md +488 -0
  56. package/docs/USAGE.md +422 -0
  57. package/docs/YAML_CONFIG.md +273 -0
  58. package/docs/mcp-proxy-config.example.yaml +62 -0
  59. package/package.json +17 -10
  60. package/dist/proxy/cloudLlmHubProxy.d.ts +0 -2
  61. package/dist/proxy/cloudLlmHubProxy.d.ts.map +0 -1
  62. package/dist/proxy/cloudLlmHubProxy.js +0 -3
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SHUTDOWN_REMINDER = void 0;
4
+ exports.createProxyTools = createProxyTools;
5
+ // src/mcp/tools.ts
6
+ const zod_1 = require("zod");
7
+ const config_js_1 = require("../lib/config.js");
8
+ const configs_js_1 = require("./configs.js");
9
+ const supervisor_js_1 = require("./supervisor.js");
10
+ /**
11
+ * The thing the client has to keep being told.
12
+ *
13
+ * A running proxy holds a TCP port and a live credential for the destination.
14
+ * The client here is a language model reading tool descriptions selectively,
15
+ * so this appears in the description of `proxy_start`, in the text that comes
16
+ * back with the URL, and in the server's own instructions. Three places for one
17
+ * sentence is not redundancy — it is the difference between a reminder that is
18
+ * read when the work starts and one that is read when the work finishes.
19
+ */
20
+ exports.SHUTDOWN_REMINDER = 'When you are finished with this proxy, call proxy_stop. ' +
21
+ 'A running proxy holds a TCP port and a live credential for its destination ' +
22
+ 'until it is stopped or the session ends.';
23
+ const text = (body) => ({
24
+ content: [{ type: 'text', text: body }],
25
+ });
26
+ function createProxyTools(supervisor, configDir = (0, configs_js_1.proxyConfigDir)()) {
27
+ return [
28
+ {
29
+ name: 'proxy_configs',
30
+ title: 'List the proxy configs available',
31
+ description: 'List the proxy configurations on this machine, by the name ' +
32
+ 'proxy_start takes. Each one already carries its destination, target ' +
33
+ 'URL, default headers and timeouts, so starting a proxy is choosing a ' +
34
+ 'name — not assembling settings. Call this first; the names cannot be ' +
35
+ 'guessed.',
36
+ inputSchema: {},
37
+ handler: async () => {
38
+ const configs = (0, configs_js_1.listProxyConfigs)(configDir);
39
+ if (configs.length === 0) {
40
+ return text(`No proxy configs found in ${configDir}.`);
41
+ }
42
+ return text([
43
+ `Proxy configs in ${configDir}:`,
44
+ ...configs.map((c) => {
45
+ const about = (0, configs_js_1.describeConfig)(c);
46
+ const where = c.destination
47
+ ? ` — destination ${c.destination}`
48
+ : '';
49
+ return ` ${c.name}${where}${about ? `\n ${about}` : ''}`;
50
+ }),
51
+ '',
52
+ 'Start one with proxy_start { "config": "<name>" }.',
53
+ ].join('\n'));
54
+ },
55
+ },
56
+ {
57
+ name: 'proxy_start',
58
+ title: 'Start an authenticating proxy',
59
+ description: 'Start a local proxy from one of the configs proxy_configs lists. The ' +
60
+ 'config supplies the destination, target URL, default headers and ' +
61
+ 'timeouts — including any credentials, which stay in the config and ' +
62
+ 'are never passed through here. The port is NOT taken from the config: ' +
63
+ 'a free one is bound instead, so several proxies can run at once, and ' +
64
+ `the URL that comes back is the one actually bound. ${exports.SHUTDOWN_REMINDER}`,
65
+ inputSchema: {
66
+ config: zod_1.z
67
+ .string()
68
+ .describe('Name of a proxy config, as proxy_configs lists it (for example "nvcr_d24").'),
69
+ idleTimeoutMs: zod_1.z
70
+ .number()
71
+ .optional()
72
+ .describe(`Stop the proxy after this long with no request. Defaults to ${supervisor_js_1.DEFAULT_IDLE_TIMEOUT_MS} ms; 0 disables it. This is a backstop, not a substitute for proxy_stop.`),
73
+ },
74
+ handler: async (args) => {
75
+ const name = String(args.config);
76
+ const file = (0, configs_js_1.resolveProxyConfig)(name, configDir);
77
+ const started = await supervisor.start({
78
+ name,
79
+ config: (0, config_js_1.loadConfig)(file),
80
+ idleTimeoutMs: args.idleTimeoutMs,
81
+ });
82
+ return text([
83
+ `Proxy running at ${started.url}`,
84
+ ` config: ${started.name}`,
85
+ ` instanceId: ${started.instanceId}`,
86
+ ` destination: ${started.destination}`,
87
+ '',
88
+ exports.SHUTDOWN_REMINDER,
89
+ ].join('\n'));
90
+ },
91
+ },
92
+ {
93
+ name: 'proxy_stop',
94
+ title: 'Stop a proxy this session started',
95
+ description: 'Stop a proxy started in this session, freeing its port and releasing ' +
96
+ 'its credential. With no instanceId, stops every proxy this session ' +
97
+ 'started. Proxies belonging to other sessions are never touched — ' +
98
+ 'proxy_status shows them, and they are theirs to stop.',
99
+ inputSchema: {
100
+ instanceId: zod_1.z
101
+ .string()
102
+ .optional()
103
+ .describe('Which proxy to stop. Omit to stop all of this session’s.'),
104
+ },
105
+ handler: async (args) => {
106
+ const stopped = await supervisor.stop(args.instanceId);
107
+ if (stopped.length === 0) {
108
+ return text(args.instanceId
109
+ ? `Nothing stopped: this session does not own a proxy with instanceId ${args.instanceId}. Use proxy_status to see what is running.`
110
+ : 'Nothing stopped: this session has no proxies running.');
111
+ }
112
+ return text([
113
+ `Stopped ${stopped.length} ${stopped.length === 1 ? 'proxy' : 'proxies'}:`,
114
+ ...stopped.map((s) => ` ${s.url} — config ${s.name} — port ${s.port} released`),
115
+ ].join('\n'));
116
+ },
117
+ },
118
+ {
119
+ name: 'proxy_status',
120
+ title: 'List running proxies',
121
+ description: 'List the proxies this session started, and any started by other ' +
122
+ 'sessions on this machine. Records whose process has died are pruned ' +
123
+ 'when this is read, so what it reports is what is actually running.',
124
+ inputSchema: {},
125
+ handler: async () => {
126
+ const mine = supervisor.mine();
127
+ const others = supervisor.others();
128
+ if (mine.length === 0 && others.length === 0) {
129
+ return text('No proxies are running on this machine.');
130
+ }
131
+ const lines = [];
132
+ if (mine.length > 0) {
133
+ lines.push('Started by this session:');
134
+ for (const m of mine) {
135
+ lines.push(` ${m.url} — config ${m.name} (destination ${m.destination}) — instanceId ${m.instanceId}`);
136
+ }
137
+ lines.push('', exports.SHUTDOWN_REMINDER);
138
+ }
139
+ else {
140
+ lines.push('This session has no proxies running.');
141
+ }
142
+ if (others.length > 0) {
143
+ lines.push('', 'Started by another session (not yours to stop):');
144
+ for (const o of others) {
145
+ lines.push(` ${o.url} — config ${o.config} (destination ${o.destination}) — pid ${o.pid}`);
146
+ }
147
+ }
148
+ return text(lines.join('\n'));
149
+ },
150
+ },
151
+ ];
152
+ }
@@ -6,42 +6,18 @@
6
6
  */
7
7
  import { AuthBroker } from '@mcp-abap-adt/auth-broker';
8
8
  import { type ProxyConfig } from '../lib/config.js';
9
- import type { RoutingDecision } from '../router/headerAnalyzer.js';
10
9
  /**
11
10
  * Check if error messages should be written to stderr
12
11
  * Only output in verbose mode and not in test environment
13
12
  */
14
13
  export declare function shouldWriteStderr(): boolean;
15
- export interface GenericProxyRequest {
16
- method: string;
17
- url?: string;
18
- data?: unknown;
19
- id?: string | number | null;
20
- [key: string]: unknown;
21
- }
22
- export type ProxyRequest = GenericProxyRequest;
23
- export interface ProxyResponse {
24
- jsonrpc: string;
25
- id?: string | number | null;
26
- result?: unknown;
27
- error?: {
28
- code: number;
29
- message: string;
30
- data?: unknown;
31
- };
32
- }
33
14
  /**
34
15
  * BTP Proxy Client
35
16
  */
36
17
  export declare class BtpProxy {
37
- private axiosInstance;
38
18
  private defaultBtpAuthBroker;
39
19
  private btpAuthBrokers;
40
- private tokenCache;
41
- private refreshTimers;
42
- private readonly TOKEN_CACHE_TTL;
43
- private readonly REFRESH_LEAD_MS;
44
- private circuitBreaker;
20
+ private readonly credentials;
45
21
  private config;
46
22
  private unsafe;
47
23
  constructor(defaultBtpAuthBroker: AuthBroker, config?: Partial<ProxyConfig>);
@@ -61,63 +37,38 @@ export declare class BtpProxy {
61
37
  */
62
38
  private ensureSessionServiceUrl;
63
39
  /**
64
- * Get JWT token for BTP destination from auth-broker with retry and token refresh
65
- * @param destination Destination name
66
- * @param forceRefresh Force token refresh
67
- */
68
- getJwtToken(destination: string, forceRefresh?: boolean): Promise<string>;
69
- /**
70
- * Decode JWT exp claim (seconds since epoch). Returns null if not parseable.
71
- */
72
- private decodeJwtExp;
73
- /**
74
- * Cache token with expiry derived from JWT, and schedule proactive refresh
75
- * 5 minutes before expiry.
76
- */
77
- private cacheToken;
78
- /**
79
- * Schedule background refresh REFRESH_LEAD_MS before token expiry.
80
- */
81
- private scheduleProactiveRefresh;
82
- /**
83
- * Background refresh via auth-broker's refreshToken grant.
40
+ * The `Authorization` header value for a destination, or `null` when this
41
+ * credential is not a header at all.
42
+ *
43
+ * A complete header value, not a token: the caller puts it on the request as
44
+ * it stands. The credential is asked every time because it renews behind this
45
+ * call — a cache here would serve the stale token and hide the renewal the
46
+ * broker exists to do, which is precisely what the token cache, the JWT `exp`
47
+ * decoder and the per-destination refresh timer that used to live here did.
48
+ *
49
+ * Retried, because a refusal here is not always an answer. In the standalone
50
+ * proxy an authentication failure is FATAL — it exits so something can start
51
+ * it again — so without this a single transient UAA 503 kills a proxy that
52
+ * used to ride it out. Only failures that can get better are retried;
53
+ * `isRetryableError` says which, and a missing service key is not one of
54
+ * them, so it fails on the first attempt instead of three times slower.
84
55
  */
85
- private proactiveRefresh;
56
+ getAuthorizationHeader(destination: string): Promise<string | null>;
86
57
  /**
87
- * Cancel all pending refresh timers. Call on shutdown.
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.
88
64
  */
65
+ private explainAuthFailure;
89
66
  dispose(): void;
90
67
  /**
91
68
  * Get the target service URL for a destination.
92
69
  * Priority: config.targetUrl > service key's serviceUrl
93
70
  */
94
71
  getTargetUrl(destination: string): Promise<string>;
95
- /**
96
- * Helper function to extract string value from header (handles arrays)
97
- */
98
- private getHeaderValue;
99
- /**
100
- * Build proxy request with JWT token (BTP authentication only)
101
- *
102
- * Process flow:
103
- *
104
- * 1. BTP Authentication (XSUAA):
105
- * 1.1 If x-sap-destination header exists:
106
- * - Check map for broker with key = destination, get or create, save to map
107
- * - Get token from xsuaa broker
108
- * - Add/replace Authorization: Bearer <token> header
109
- * 1.2 If header doesn't exist but --btp parameter exists:
110
- * - Use destination from parameter, get or create broker, save to map
111
- * - Get token from xsuaa broker
112
- * - Add/replace Authorization: Bearer <token> header
113
- * 1.3 If neither header nor parameter:
114
- * - Do nothing, pass request further
115
- */
116
- private buildProxyRequest;
117
- /**
118
- * Proxy MCP request to target server with retry, circuit breaker, and error handling
119
- */
120
- proxyRequest(originalRequest: ProxyRequest, routingDecision: RoutingDecision, originalHeaders: Record<string, string | string[] | undefined>): Promise<ProxyResponse>;
121
72
  /**
122
73
  * Create a new BtpProxy instance
123
74
  */
@@ -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;AAgBrE,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AA8DnE;;;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;IAwB3D;;;;OAIG;YACW,wBAAwB;IAiGtC;;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;CAuC7E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAEnB"}
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;AAOrE,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA+DhE;;;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;IAiGtC;;OAEG;YACW,uBAAuB;IAyFrC;;;;;;;;;;;;;;;;OAgBG;IACG,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA+BzE;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAuBnB,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;CAuC7E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAEnB"}