@mcp-abap-adt/proxy 1.0.3 → 1.2.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 CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - 2026-04-20
11
+
12
+ ### Added
13
+ - **Proactive JWT token refresh** — proxy now decodes the `exp` claim from the JWT and refreshes the token in the background 5 minutes before expiry via `authBroker.refreshToken()` (OAuth2 refresh_token grant, preserves role assignment). Eliminates latency spikes from on-demand 401 refreshes while keeping the reactive 401/403 fallback.
14
+
15
+ ### Changed
16
+ - **@mcp-abap-adt/interfaces 7.0** — updated from v6.0 to v7.0
17
+ - **typescript 6.0.3**, **@biomejs/biome 2.4.12**, **@modelcontextprotocol/inspector 0.21.2**, **axios 1.15.1**, **@inquirer/prompts 8.4.2** — patch bumps
18
+
19
+ ## [1.1.0] - 2026-04-12
20
+
21
+ ### Changed
22
+ - **TypeScript 6.0** — migrated from TypeScript 5.9 to 6.0; added explicit `rootDir: "./src"` to `tsconfig.json` (required by TS6)
23
+ - **@mcp-abap-adt/interfaces 6.0** — updated from v5.0 to v6.0
24
+ - **@modelcontextprotocol/sdk 1.29** — updated from v1.26 to v1.29
25
+ - **@modelcontextprotocol/inspector 0.21** — updated from v0.20 to v0.21
26
+ - **Biome 2.4** — updated from v2.3 to v2.4
27
+ - **Jest 30.3** — updated from v30.2 to v30.3
28
+ - **axios 1.15** — updated from v1.13 to v1.15
29
+ - **ts-jest 29.4.9**, **@types/node 25.6**, **@inquirer/prompts 8.4**, **rimraf 6.1.3** — minor/patch bumps
30
+
10
31
  ## [1.0.3] - 2026-04-07
11
32
 
12
33
  ### Fixed
package/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # @mcp-abap-adt/proxy
2
+ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
2
3
 
3
4
  MCP proxy server for SAP ABAP ADT - proxies local requests to MCP servers with JWT authentication.
4
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAuBH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAqBlC;;GAEG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,CAAW;gBAEhB,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAoDlE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAuE1B;;OAEG;YACW,eAAe;IAgG7B;;OAEG;YACW,kBAAkB;IA8ChC;;OAEG;YACW,cAAc;IA6T5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAmBhC;AAGD,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAuBH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAqBlC;;GAEG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,CAAW;gBAEhB,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAoDlE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAuE1B;;OAEG;YACW,eAAe;IAgG7B;;OAEG;YACW,kBAAkB;IA8ChC;;OAEG;YACW,cAAc;IA6T5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBhC;AAGD,eAAe,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -544,6 +544,7 @@ Authorization source: ${authSourceObj}`;
544
544
  * Shutdown server
545
545
  */
546
546
  async shutdown() {
547
+ this.btpProxy?.dispose();
547
548
  try {
548
549
  await this.server.close();
549
550
  }
@@ -38,7 +38,9 @@ export declare class BtpProxy {
38
38
  private defaultBtpAuthBroker;
39
39
  private btpAuthBrokers;
40
40
  private tokenCache;
41
+ private refreshTimers;
41
42
  private readonly TOKEN_CACHE_TTL;
43
+ private readonly REFRESH_LEAD_MS;
42
44
  private circuitBreaker;
43
45
  private config;
44
46
  private unsafe;
@@ -64,6 +66,27 @@ export declare class BtpProxy {
64
66
  * @param forceRefresh Force token refresh
65
67
  */
66
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.
84
+ */
85
+ private proactiveRefresh;
86
+ /**
87
+ * Cancel all pending refresh timers. Call on shutdown.
88
+ */
89
+ dispose(): void;
67
90
  /**
68
91
  * Get the target service URL for a destination.
69
92
  * Priority: config.targetUrl > service key's serviceUrl
@@ -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,QAAQ,CAAC,eAAe,CAAkB;IAClD,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;IAqHlB;;;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"}
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;IAOtB;;;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"}
@@ -58,7 +58,9 @@ class BtpProxy {
58
58
  defaultBtpAuthBroker;
59
59
  btpAuthBrokers = new Map();
60
60
  tokenCache = new Map();
61
- TOKEN_CACHE_TTL = 30 * 60 * 1000; // 30 minutes
61
+ refreshTimers = new Map();
62
+ TOKEN_CACHE_TTL = 30 * 60 * 1000; // fallback if JWT has no exp
63
+ REFRESH_LEAD_MS = 5 * 60 * 1000; // refresh 5 min before expiry
62
64
  circuitBreaker;
63
65
  config;
64
66
  unsafe;
@@ -349,11 +351,7 @@ class BtpProxy {
349
351
  });
350
352
  // Get token from auth-broker
351
353
  const token = await authBroker.getToken(destination);
352
- // Cache token (assume it's valid for 30 minutes)
353
- this.tokenCache.set(destination, {
354
- token,
355
- expiresAt: Date.now() + this.TOKEN_CACHE_TTL,
356
- });
354
+ this.cacheToken(destination, token);
357
355
  logger_js_1.logger?.debug('Retrieved JWT token from auth-broker', {
358
356
  type: 'JWT_TOKEN_RETRIEVED',
359
357
  destination,
@@ -404,6 +402,79 @@ class BtpProxy {
404
402
  throw new Error(errorMessage);
405
403
  }
406
404
  }
405
+ /**
406
+ * Decode JWT exp claim (seconds since epoch). Returns null if not parseable.
407
+ */
408
+ decodeJwtExp(token) {
409
+ const parts = token.split('.');
410
+ if (parts.length < 2)
411
+ return null;
412
+ try {
413
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
414
+ return typeof payload.exp === 'number' ? payload.exp : null;
415
+ }
416
+ catch {
417
+ return null;
418
+ }
419
+ }
420
+ /**
421
+ * Cache token with expiry derived from JWT, and schedule proactive refresh
422
+ * 5 minutes before expiry.
423
+ */
424
+ cacheToken(destination, token) {
425
+ const exp = this.decodeJwtExp(token);
426
+ const expiresAt = exp !== null ? exp * 1000 : Date.now() + this.TOKEN_CACHE_TTL;
427
+ this.tokenCache.set(destination, { token, expiresAt });
428
+ this.scheduleProactiveRefresh(destination, expiresAt);
429
+ }
430
+ /**
431
+ * Schedule background refresh REFRESH_LEAD_MS before token expiry.
432
+ */
433
+ scheduleProactiveRefresh(destination, expiresAt) {
434
+ const existing = this.refreshTimers.get(destination);
435
+ if (existing)
436
+ clearTimeout(existing);
437
+ const delay = expiresAt - Date.now() - this.REFRESH_LEAD_MS;
438
+ if (delay <= 0) {
439
+ // Token already within lead window or expired; skip scheduling.
440
+ this.refreshTimers.delete(destination);
441
+ return;
442
+ }
443
+ const timer = setTimeout(() => {
444
+ this.refreshTimers.delete(destination);
445
+ this.proactiveRefresh(destination).catch((err) => {
446
+ logger_js_1.logger?.warn('Proactive token refresh failed, will retry reactively', {
447
+ type: 'JWT_TOKEN_PROACTIVE_REFRESH_ERROR',
448
+ destination,
449
+ error: err instanceof Error ? err.message : String(err),
450
+ });
451
+ });
452
+ }, delay);
453
+ // Don't keep the event loop alive just for refresh timers.
454
+ timer.unref?.();
455
+ this.refreshTimers.set(destination, timer);
456
+ }
457
+ /**
458
+ * Background refresh via auth-broker's refreshToken grant.
459
+ */
460
+ async proactiveRefresh(destination) {
461
+ logger_js_1.logger?.info('Proactively refreshing JWT token', {
462
+ type: 'JWT_TOKEN_PROACTIVE_REFRESH',
463
+ destination,
464
+ });
465
+ const broker = await this.getOrCreateBtpAuthBroker(destination);
466
+ const token = await broker.refreshToken(destination);
467
+ this.cacheToken(destination, token);
468
+ }
469
+ /**
470
+ * Cancel all pending refresh timers. Call on shutdown.
471
+ */
472
+ dispose() {
473
+ for (const timer of this.refreshTimers.values()) {
474
+ clearTimeout(timer);
475
+ }
476
+ this.refreshTimers.clear();
477
+ }
407
478
  /**
408
479
  * Get the target service URL for a destination.
409
480
  * Priority: config.targetUrl > service key's serviceUrl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/proxy",
3
- "version": "1.0.3",
3
+ "version": "1.2.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",
@@ -44,9 +44,9 @@
44
44
  "access": "public"
45
45
  },
46
46
  "devDependencies": {
47
- "@biomejs/biome": "^2.3.15",
47
+ "@biomejs/biome": "^2.4.12",
48
48
  "@jest/globals": "^30.2.0",
49
- "@modelcontextprotocol/inspector": "^0.20.0",
49
+ "@modelcontextprotocol/inspector": "^0.21.2",
50
50
  "@types/jest": "^30.0.0",
51
51
  "@types/js-yaml": "^4.0.9",
52
52
  "@types/node": "^25.2.3",
@@ -61,18 +61,18 @@
61
61
  "rimraf": "^6.1.2",
62
62
  "ts-jest": "^29.2.5",
63
63
  "tsx": "^4.21.0",
64
- "typescript": "^5.9.2"
64
+ "typescript": "^6.0.3"
65
65
  },
66
66
  "dependencies": {
67
- "@inquirer/prompts": "^8.3.2",
67
+ "@inquirer/prompts": "^8.4.2",
68
68
  "@mcp-abap-adt/auth-broker": "^1.0.5",
69
69
  "@mcp-abap-adt/auth-providers": "^1.0.5",
70
70
  "@mcp-abap-adt/auth-stores": "^1.0.4",
71
71
  "@mcp-abap-adt/header-validator": "^0.1.8",
72
- "@mcp-abap-adt/interfaces": "^5.0.0",
72
+ "@mcp-abap-adt/interfaces": "^7.0.0",
73
73
  "@mcp-abap-adt/logger": "^0.1.4",
74
74
  "@modelcontextprotocol/sdk": "^1.26.0",
75
- "axios": "^1.13.5",
75
+ "axios": "^1.15.1",
76
76
  "js-yaml": "^4.1.1",
77
77
  "zod": "^4.3.6"
78
78
  },