@mcp-abap-adt/auth-broker 0.2.16 → 0.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 CHANGED
@@ -11,6 +11,27 @@ Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the co
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.3.0] - 2025-12-31
15
+
16
+ ### Added
17
+ - **CLI**: Auto-detect service key format (ABAP vs XSUAA) based on content structure
18
+ - **CLI**: Support for "credentials" wrapper in service keys (common in SAP BTP)
19
+ - **CLI**: Fallback parsing when stores fail to parse service key
20
+ - **CLI**: Log authorization URL and redirect URI for easier debugging
21
+ - **Documentation**: Added `CLAUDE.md` for Claude Code project guidance
22
+
23
+ ### Changed
24
+ - **Dependencies**: Updated `@mcp-abap-adt/auth-stores` from `^0.2.10` to `^0.3.0`
25
+
26
+ ### Fixed
27
+ - **CLI**: Filter placeholder `<SERVICE_URL>` from output when not available
28
+
29
+ ## [0.2.17] - 2025-12-31
30
+
31
+ ### Changed
32
+ - **Dependencies**: Depend on the published `@mcp-abap-adt/auth-broker` `^0.2.17` package to keep the CLI wiring aligned with the latest release.
33
+ - **Dependencies**: Bump `@mcp-abap-adt/interfaces` to `^0.2.15` so consumers pick up the current interface contracts.
34
+
14
35
  ## [0.2.16] - 2025-12-31
15
36
 
16
37
  ### Changed
package/bin/mcp-auth.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  AbapSessionStore,
30
30
  XsuaaServiceKeyStore,
31
31
  XsuaaSessionStore,
32
+ JsonFileHandler,
32
33
  } from '@mcp-abap-adt/auth-stores';
33
34
  import {
34
35
  AuthorizationCodeProvider,
@@ -378,9 +379,12 @@ async function main() {
378
379
  destination = path.basename(resolvedEnvPath, path.extname(resolvedEnvPath));
379
380
  }
380
381
 
382
+ // Store raw service key JSON for fallback parsing
383
+ let rawServiceKeyJson: any = null;
384
+
381
385
  if (options.serviceKeyPath) {
382
386
  const resolvedServiceKeyPath = path.resolve(options.serviceKeyPath);
383
- const serviceKeyDir = path.dirname(resolvedServiceKeyPath);
387
+ let serviceKeyDir = path.dirname(resolvedServiceKeyPath);
384
388
 
385
389
  // Check if service key file exists
386
390
  if (!fs.existsSync(resolvedServiceKeyPath)) {
@@ -396,11 +400,62 @@ async function main() {
396
400
  process.exit(1);
397
401
  }
398
402
  destination = serviceKeyFileName;
399
-
400
- // Create appropriate stores based on auth type
401
- serviceKeyStore = options.authType === 'xsuaa'
402
- ? new XsuaaServiceKeyStore(serviceKeyDir)
403
- : new AbapServiceKeyStore(serviceKeyDir);
403
+
404
+ // Determine which store to use based on service key content
405
+ // ABAP format has nested "uaa" object, XSUAA format has flat structure
406
+ let isAbapFormat = options.authType === 'abap'; // default fallback
407
+ try {
408
+ // Use JsonFileHandler to ensure consistent parsing behavior
409
+ const json = await JsonFileHandler.load(path.basename(resolvedServiceKeyPath), serviceKeyDir);
410
+
411
+ let effectiveJson = json;
412
+ if (json && json.credentials) {
413
+ console.log('🔍 Detected "credentials" wrapper -> unwrapping to temp file');
414
+ effectiveJson = json.credentials;
415
+
416
+ // Create temp file with unwrapped content to make it compatible with standard stores
417
+ const tempDir = path.join(path.dirname(resolvedOutputPath), '.tmp');
418
+ if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
419
+ const tempKeyPath = path.join(tempDir, `${destination}.json`);
420
+ fs.writeFileSync(tempKeyPath, JSON.stringify(effectiveJson, null, 2));
421
+
422
+ // Point store to temp directory so it reads the unwrapped file
423
+ serviceKeyDir = tempDir;
424
+ }
425
+
426
+ // Store raw JSON for fallback parsing
427
+ rawServiceKeyJson = effectiveJson;
428
+
429
+ if (effectiveJson) {
430
+ // If it has uaa property (even if null/string/object), XSUAA parser rejects it.
431
+ // So we must use ABAP store which expects uaa object.
432
+ if (effectiveJson.uaa) {
433
+ isAbapFormat = true;
434
+ } else {
435
+ isAbapFormat = false;
436
+ }
437
+ }
438
+ } catch (e: any) {
439
+ // If parsing fails here, let the store handle the error
440
+ }
441
+
442
+ // Create appropriate stores based on detected format
443
+ serviceKeyStore = isAbapFormat
444
+ ? new AbapServiceKeyStore(serviceKeyDir)
445
+ : new XsuaaServiceKeyStore(serviceKeyDir);
446
+
447
+ // Patch serviceKeyStore for XSUAA to ensure serviceUrl is present (AuthBroker requirement)
448
+ // even if not in the file. XSUAA doesn't strictly need it, but AuthBroker enforces it.
449
+ if (options.authType === 'xsuaa') {
450
+ const originalGetConnectionConfig = serviceKeyStore.getConnectionConfig.bind(serviceKeyStore);
451
+ serviceKeyStore.getConnectionConfig = async (dest: string) => {
452
+ const config = await originalGetConnectionConfig(dest);
453
+ if (config && !config.serviceUrl) {
454
+ config.serviceUrl = '<SERVICE_URL>';
455
+ }
456
+ return config;
457
+ };
458
+ }
404
459
  }
405
460
 
406
461
  if (!destination) {
@@ -443,10 +498,15 @@ async function main() {
443
498
  // Resolve serviceUrl from service key if not provided explicitly
444
499
  let actualServiceUrl = options.serviceUrl;
445
500
  if (!actualServiceUrl && serviceKeyStore) {
446
- const serviceKeyConn = await serviceKeyStore.getConnectionConfig(
447
- destination,
448
- );
449
- actualServiceUrl = serviceKeyConn?.serviceUrl;
501
+ try {
502
+ const serviceKeyConn = await serviceKeyStore.getConnectionConfig(
503
+ destination,
504
+ );
505
+ actualServiceUrl = serviceKeyConn?.serviceUrl;
506
+ } catch {
507
+ // For XSUAA, serviceUrl is optional and may not exist in service key
508
+ // This is expected - continue without serviceUrl
509
+ }
450
510
  }
451
511
 
452
512
  // For XSUAA, serviceUrl is optional - use placeholder only for AuthBroker internal work
@@ -459,17 +519,49 @@ async function main() {
459
519
 
460
520
  const sessionAuthConfig =
461
521
  await sessionStore.getAuthorizationConfig(destination);
462
- const serviceKeyAuthConfig =
463
- serviceKeyStore?.getAuthorizationConfig
464
- ? await serviceKeyStore.getAuthorizationConfig(destination)
465
- : null;
466
- const authConfig = sessionAuthConfig || serviceKeyAuthConfig;
522
+ let serviceKeyAuthConfig = null;
523
+ if (serviceKeyStore?.getAuthorizationConfig) {
524
+ try {
525
+ serviceKeyAuthConfig = await serviceKeyStore.getAuthorizationConfig(destination);
526
+ } catch (e: any) {
527
+ // Service key parsing might fail - try fallback parsing from raw JSON
528
+ console.log(`â„šī¸ Store could not parse service key, using fallback parsing`);
529
+ }
530
+ }
531
+
532
+ // Fallback: if stores failed, try to construct authConfig from raw service key JSON
533
+ let fallbackAuthConfig = null;
534
+ if (!sessionAuthConfig && !serviceKeyAuthConfig && rawServiceKeyJson) {
535
+ // XSUAA format: clientid, clientsecret, url at top level
536
+ // ABAP format: uaa.clientid, uaa.clientsecret, uaa.url
537
+ const uaa = rawServiceKeyJson.uaa || rawServiceKeyJson;
538
+ if (uaa.clientid && uaa.clientsecret && uaa.url) {
539
+ fallbackAuthConfig = {
540
+ uaaUrl: uaa.url,
541
+ uaaClientId: uaa.clientid,
542
+ uaaClientSecret: uaa.clientsecret,
543
+ };
544
+ console.log(`✅ Constructed auth config from raw service key`);
545
+ }
546
+ }
547
+
548
+ const authConfig = sessionAuthConfig || serviceKeyAuthConfig || fallbackAuthConfig;
467
549
  if (!authConfig) {
468
- throw new Error(`Authorization config not found for ${destination}`);
550
+ throw new Error(`Authorization config not found for ${destination}. Service key must contain clientid, clientsecret, and url fields.`);
469
551
  }
470
552
 
471
553
  // Default: authorization_code flow with browser
472
554
  // --credential: client_credentials flow (no browser needed)
555
+ const redirectPort = options.redirectPort || 3001;
556
+
557
+ // Log authorization URL for debugging
558
+ if (!options.credential) {
559
+ const redirectUri = `http://localhost:${redirectPort}/callback`;
560
+ const authorizationUrl = `${authConfig.uaaUrl}/oauth/authorize?client_id=${encodeURIComponent(authConfig.uaaClientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`;
561
+ console.log(`🔗 Authorization URL: ${authorizationUrl}`);
562
+ console.log(`📍 Redirect URI: ${redirectUri}`);
563
+ }
564
+
473
565
  const tokenProvider = options.credential
474
566
  ? new ClientCredentialsProvider({
475
567
  uaaUrl: authConfig.uaaUrl,
@@ -482,7 +574,7 @@ async function main() {
482
574
  clientSecret: authConfig.uaaClientSecret,
483
575
  refreshToken: authConfig.refreshToken,
484
576
  browser: options.browser,
485
- redirectPort: options.redirectPort,
577
+ redirectPort: redirectPort,
486
578
  });
487
579
 
488
580
  const broker = new AuthBroker(
@@ -508,6 +600,9 @@ async function main() {
508
600
 
509
601
  const outputServiceUrl =
510
602
  options.serviceUrl || connConfig?.serviceUrl || actualServiceUrl;
603
+
604
+ // Filter out placeholder service URL
605
+ const finalServiceUrl = outputServiceUrl === '<SERVICE_URL>' ? undefined : outputServiceUrl;
511
606
 
512
607
  // Write output file based on format
513
608
  if (options.format === 'env') {
@@ -516,7 +611,7 @@ async function main() {
516
611
  options.authType,
517
612
  token,
518
613
  authConfig?.refreshToken,
519
- outputServiceUrl,
614
+ finalServiceUrl,
520
615
  authConfig?.uaaUrl,
521
616
  authConfig?.uaaClientId,
522
617
  authConfig?.uaaClientSecret,
@@ -527,8 +622,8 @@ async function main() {
527
622
  // Show what was written
528
623
  console.log(`📋 .env file contains:`);
529
624
  if (options.authType === 'abap') {
530
- if (outputServiceUrl) {
531
- console.log(` - ${ABAP_CONNECTION_VARS.SERVICE_URL}=${outputServiceUrl}`);
625
+ if (finalServiceUrl) {
626
+ console.log(` - ${ABAP_CONNECTION_VARS.SERVICE_URL}=${finalServiceUrl}`);
532
627
  }
533
628
  console.log(
534
629
  ` - ${ABAP_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`,
@@ -539,8 +634,8 @@ async function main() {
539
634
  );
540
635
  }
541
636
  } else {
542
- if (outputServiceUrl) {
543
- console.log(` - XSUAA_MCP_URL=${outputServiceUrl}`);
637
+ if (finalServiceUrl) {
638
+ console.log(` - XSUAA_MCP_URL=${finalServiceUrl}`);
544
639
  }
545
640
  console.log(
546
641
  ` - ${XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`,
@@ -556,7 +651,7 @@ async function main() {
556
651
  resolvedOutputPath,
557
652
  token,
558
653
  authConfig?.refreshToken,
559
- outputServiceUrl,
654
+ finalServiceUrl,
560
655
  authConfig?.uaaUrl,
561
656
  authConfig?.uaaClientId,
562
657
  authConfig?.uaaClientSecret,
@@ -570,8 +665,8 @@ async function main() {
570
665
  ` - refreshToken: ${authConfig.refreshToken.substring(0, 50)}...`,
571
666
  );
572
667
  }
573
- if (outputServiceUrl) {
574
- console.log(` - serviceUrl: ${outputServiceUrl}`);
668
+ if (finalServiceUrl) {
669
+ console.log(` - serviceUrl: ${finalServiceUrl}`);
575
670
  }
576
671
  }
577
672
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-broker",
3
- "version": "0.2.16",
3
+ "version": "0.3.0",
4
4
  "description": "JWT authentication broker for MCP ABAP ADT - manages tokens based on destination headers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -56,10 +56,9 @@
56
56
  "node": ">=18.0.0"
57
57
  },
58
58
  "dependencies": {
59
- "@mcp-abap-adt/auth-broker": "file:mcp-abap-adt-auth-broker-0.2.15.tgz",
60
59
  "@mcp-abap-adt/auth-providers": "^0.2.10",
61
- "@mcp-abap-adt/auth-stores": "^0.2.10",
62
- "@mcp-abap-adt/interfaces": "^0.2.14",
60
+ "@mcp-abap-adt/auth-stores": "^0.3.0",
61
+ "@mcp-abap-adt/interfaces": "^0.2.15",
63
62
  "axios": "^1.13.2",
64
63
  "tsx": "^4.21.0"
65
64
  },