@mcp-abap-adt/auth-broker 0.2.10 → 0.2.11

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,16 @@ Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the co
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.2.11] - 2025-12-23
15
+
16
+ ### Changed
17
+ - **mcp-auth CLI**: Reworked to use AuthBroker + stores with env-first refresh fallback to service key.
18
+ - **Token provider wiring**: AuthBroker now guards optional token provider methods before invoking them.
19
+ - **Docs**: Added CLI usage details for mcp-auth in README and usage guide.
20
+
21
+ ### Updated
22
+ - **Dependencies**: Bumped `@mcp-abap-adt/interfaces` to ^0.2.9 and `@mcp-abap-adt/auth-providers` to latest.
23
+
14
24
  ## [0.2.10] - 2025-12-22
15
25
 
16
26
  ### Changed
package/README.md CHANGED
@@ -602,29 +602,39 @@ When UAA credentials are available in session, `AuthBroker` automatically uses d
602
602
  - If direct UAA request fails and `tokenProvider` is available, broker automatically falls back to provider
603
603
  - Provider is useful for browser authentication or alternative authentication flows
604
604
 
605
- ### Utility Script
605
+ ### CLI: mcp-auth
606
606
 
607
- Generate `.env` files from service keys:
607
+ Generate or refresh `.env`/JSON output using AuthBroker + stores:
608
608
 
609
609
  ```bash
610
- npm run generate-env <destination> [service-key-path] [session-path]
610
+ mcp-auth --service-key <path> --output <path> [--env <path>] [--type abap|xsuaa] [--browser system|chrome|edge|firefox|none] [--format json|env]
611
611
  ```
612
612
 
613
+ **Behavior:**
614
+ - If `--env` is provided and exists, refresh token is attempted first.
615
+ - If refresh fails (or env is missing), service key auth is used.
616
+ - If `--browser` is omitted → client_credentials (no browser).
617
+ - If `--browser` is provided → authorization_code (browser OAuth2).
618
+
613
619
  **Examples:**
614
620
  ```bash
615
- # Generate .env from service key (auto-detect paths)
616
- npm run generate-env mcp
621
+ # XSUAA: try refresh from env, fallback to service key
622
+ mcp-auth --env ./mcp.env --service-key ./mcp.json --output ./mcp.env --type xsuaa
617
623
 
618
- # Specify paths explicitly
619
- npm run generate-env mcp ./mcp.json ./mcp.env
624
+ # ABAP: browser auth (system browser)
625
+ mcp-auth --service-key ./abap.json --output ./abap.env --type abap --browser system
620
626
 
621
- # Use absolute paths
622
- npm run generate-env TRIAL ~/.config/mcp-abap-adt/service-keys/TRIAL.json ~/.config/mcp-abap-adt/sessions/TRIAL.env
627
+ # XSUAA: client_credentials (no browser)
628
+ mcp-auth --service-key ./mcp.json --output ./mcp.env --type xsuaa
623
629
  ```
624
630
 
625
- The script automatically detects service key type (ABAP or XSUAA) and uses the appropriate authentication flow:
626
- - **ABAP**: Opens browser for OAuth2 authorization code flow
627
- - **XSUAA**: Uses client_credentials grant type (no browser required)
631
+ ### Utility Script
632
+
633
+ Generate `.env` files from service keys:
634
+
635
+ ```bash
636
+ npm run generate-env <destination> [service-key-path] [session-path]
637
+ ```
628
638
 
629
639
  ## Testing
630
640
 
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Wrapper script for mcp-auth.ts
4
+ * Uses npx tsx to run TypeScript file
5
+ */
6
+
7
+ const { spawn } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ // Get absolute path to TypeScript file
12
+ const scriptPath = path.resolve(__dirname, 'mcp-auth.ts');
13
+
14
+ // Check if tsx is available locally, otherwise use npx
15
+ const localTsx = path.join(__dirname, '..', 'node_modules', '.bin', 'tsx');
16
+ const useLocalTsx = fs.existsSync(localTsx);
17
+
18
+ let command;
19
+ let args;
20
+
21
+ if (useLocalTsx) {
22
+ // Use local tsx
23
+ command = process.platform === 'win32' ? `${localTsx}.cmd` : localTsx;
24
+ args = [scriptPath, ...process.argv.slice(2)];
25
+ } else {
26
+ // Use npx tsx
27
+ command = 'npx';
28
+ args = ['tsx', scriptPath, ...process.argv.slice(2)];
29
+ }
30
+
31
+ // Run the TypeScript file
32
+ const child = spawn(command, args, {
33
+ stdio: 'inherit',
34
+ shell: false, // Don't use shell to avoid security warnings
35
+ env: process.env,
36
+ });
37
+
38
+ child.on('error', (error) => {
39
+ console.error(`Error: ${error.message}`);
40
+ if (error.code === 'ENOENT') {
41
+ console.error('Please install tsx: npm install -g tsx');
42
+ }
43
+ process.exit(1);
44
+ });
45
+
46
+ child.on('exit', (code) => {
47
+ process.exit(code || 0);
48
+ });
49
+
@@ -0,0 +1,654 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * MCP Auth - Get tokens and generate .env files from service keys
4
+ *
5
+ * Usage:
6
+ * mcp-auth --service-key <path> --output <path> [--env <path>] [--type abap|xsuaa] [--browser none|chrome|edge|firefox|system] [--format json|env]
7
+ *
8
+ * Examples:
9
+ * # Generate .env file (default)
10
+ * mcp-auth --service-key ./service-key.json --output ./mcp.env --type xsuaa --browser none
11
+ * mcp-auth --env ./mcp.env --service-key ./service-key.json --output ./mcp.env --type xsuaa --browser system
12
+ *
13
+ * # Get tokens in JSON format
14
+ * mcp-auth --service-key ./abap-key.json --output ./tokens.json --type abap --browser system --format json
15
+ *
16
+ * # Generate .env file for ABAP
17
+ * mcp-auth --service-key ./abap-key.json --output ./abap.env --type abap --browser system --format env
18
+ */
19
+
20
+ import * as path from 'path';
21
+ import * as fs from 'fs';
22
+ // Use require for CommonJS dist files with absolute path
23
+ const distPath = path.resolve(__dirname, '..', 'dist', 'index.js');
24
+ const { AuthBroker } = require(distPath);
25
+ import {
26
+ AbapServiceKeyStore,
27
+ AbapSessionStore,
28
+ XsuaaServiceKeyStore,
29
+ XsuaaSessionStore,
30
+ } from '@mcp-abap-adt/auth-stores';
31
+ import {
32
+ AuthorizationCodeProvider,
33
+ ClientCredentialsProvider,
34
+ } from '@mcp-abap-adt/auth-providers';
35
+ import type {
36
+ IAuthorizationConfig,
37
+ ITokenProvider,
38
+ ITokenProviderOptions,
39
+ ITokenProviderResult,
40
+ } from '@mcp-abap-adt/interfaces';
41
+ import {
42
+ ABAP_CONNECTION_VARS,
43
+ ABAP_AUTHORIZATION_VARS,
44
+ XSUAA_CONNECTION_VARS,
45
+ XSUAA_AUTHORIZATION_VARS,
46
+ } from '@mcp-abap-adt/auth-stores';
47
+
48
+ interface McpAuthOptions {
49
+ serviceKeyPath?: string; // Optional if env file is provided
50
+ envFilePath?: string;
51
+ outputFile: string;
52
+ authType: 'abap' | 'xsuaa';
53
+ browser?: string; // undefined = client_credentials, any value = browser auth
54
+ format: 'json' | 'env';
55
+ serviceUrl?: string;
56
+ redirectPort?: number; // Port for OAuth redirect URI (default: 3001)
57
+ }
58
+
59
+ function getVersion(): string {
60
+ try {
61
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
62
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
63
+ return packageJson.version || 'unknown';
64
+ } catch {
65
+ return 'unknown';
66
+ }
67
+ }
68
+
69
+ function showHelp(): void {
70
+ console.log('MCP Auth - Get tokens and generate .env files from service keys');
71
+ console.log('');
72
+ console.log('Usage:');
73
+ console.log(' mcp-auth --service-key <path> --output <path> [options]');
74
+ console.log('');
75
+ console.log('Required Options:');
76
+ console.log(' --output <path> Output file path');
77
+ console.log('');
78
+ console.log('Service Key or Env (one required):');
79
+ console.log(' --service-key <path> Path to service key JSON file');
80
+ console.log(' --env <path> Path to existing .env file (used for refresh token)');
81
+ console.log('');
82
+ console.log('Optional Options:');
83
+ console.log(' --type <type> Auth type: abap or xsuaa (default: abap)');
84
+ console.log(' --browser <browser> Browser auth:');
85
+ console.log(' - none: Show URL and wait for callback (no browser)');
86
+ console.log(' - auto: Try to open browser, fallback to showing URL (like cf login)');
87
+ console.log(' - system/chrome/edge/firefox: Open specific browser');
88
+ console.log(' - headless: Same as none (show URL and wait)');
89
+ console.log(' If not specified: uses client_credentials (clientId/clientSecret)');
90
+ console.log(' --format <format> Output format: json or env (default: env)');
91
+ console.log(' --service-url <url> Service URL (SAP URL for ABAP, MCP URL for XSUAA). For XSUAA, optional.');
92
+ console.log(' --redirect-port <port> Port for OAuth redirect URI (default: 3001). Must match XSUAA redirect-uris config.');
93
+ console.log('');
94
+ console.log(' --version, -v Show version number');
95
+ console.log(' --help, -h Show this help message');
96
+ console.log('');
97
+ console.log('Examples:');
98
+ console.log(' # XSUAA with client_credentials (--browser not specified)');
99
+ console.log(' mcp-auth --service-key ./service-key.json --output ./mcp.env --type xsuaa');
100
+ console.log('');
101
+ console.log(' # XSUAA using existing .env refresh token, fallback to service key');
102
+ console.log(' mcp-auth --env ./mcp.env --service-key ./service-key.json --output ./mcp.env --type xsuaa');
103
+ console.log('');
104
+ console.log(' # XSUAA with browser OAuth2 (show URL, don\'t open browser)');
105
+ console.log(' mcp-auth --service-key ./service-key.json --output ./mcp.env --type xsuaa --browser none');
106
+ console.log('');
107
+ console.log(' # XSUAA with browser OAuth2 (auto - try to open browser, like cf login)');
108
+ console.log(' mcp-auth --service-key ./service-key.json --output ./mcp.env --type xsuaa --browser auto');
109
+ console.log('');
110
+ console.log(' # XSUAA with browser OAuth2 (custom redirect port, e.g., 8080)');
111
+ console.log(' mcp-auth --service-key ./service-key.json --output ./mcp.env --type xsuaa --browser auto --redirect-port 8080');
112
+ console.log('');
113
+ console.log(' # ABAP with client_credentials (--browser not specified)');
114
+ console.log(' mcp-auth --service-key ./abap-key.json --output ./abap.env --type abap');
115
+ console.log('');
116
+ console.log(' # ABAP with browser OAuth2');
117
+ console.log(' mcp-auth --service-key ./abap-key.json --output ./abap.env --type abap --browser system');
118
+ console.log('');
119
+ console.log('Notes:');
120
+ console.log(' - --type determines the provider (xsuaa or abap)');
121
+ console.log(' - If --env is provided and file exists, refresh token is attempted first');
122
+ console.log(' - If refresh fails or env file is missing, service key auth is used');
123
+ console.log(' - Authentication method:');
124
+ console.log(' * --browser NOT specified → client_credentials (clientId/clientSecret, no browser)');
125
+ console.log(' * --browser none/headless → Show URL in console and wait for callback');
126
+ console.log(' * --browser auto → Try to open browser (like cf login), fallback to showing URL');
127
+ console.log(' * --browser system/chrome/edge/firefox → Open specific browser for OAuth2');
128
+ console.log(' - Both providers (xsuaa and abap) support both methods');
129
+ console.log(' - --redirect-port: Port for OAuth redirect URI (default: 3001)');
130
+ console.log(' * Must match redirect_uri configured in XSUAA/ABAP OAuth2 settings');
131
+ console.log(' * Common values: 3001 (default), 8080 (SAP examples)');
132
+ console.log(' - For XSUAA, serviceUrl (MCP URL) is optional - can be provided via --service-url or service key');
133
+ console.log(' - For ABAP, serviceUrl (SAP URL) is required - can be provided via --service-url or service key');
134
+ console.log(' - SAP_URL/XSUAA_MCP_URL is written to .env (from --service-url, service key, or placeholder)');
135
+ }
136
+
137
+ function parseArgs(): McpAuthOptions | null {
138
+ const args = process.argv.slice(2);
139
+
140
+ // Handle --version and --help first
141
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
142
+ showHelp();
143
+ process.exit(0);
144
+ }
145
+
146
+ if (args.includes('--version') || args.includes('-v')) {
147
+ console.log(getVersion());
148
+ process.exit(0);
149
+ }
150
+
151
+ let serviceKeyPath: string | undefined;
152
+ let envFilePath: string | undefined;
153
+ let outputFile: string | undefined;
154
+ let authType: 'abap' | 'xsuaa' = 'abap';
155
+ let browser: string | undefined; // undefined = client_credentials, any value = browser auth
156
+ let format: 'json' | 'env' = 'env';
157
+ let serviceUrl: string | undefined;
158
+ let redirectPort: number | undefined;
159
+
160
+ // Parse arguments
161
+ for (let i = 0; i < args.length; i++) {
162
+ if (args[i] === '--service-key' && i + 1 < args.length) {
163
+ serviceKeyPath = args[i + 1];
164
+ i++;
165
+ } else if (args[i] === '--env' && i + 1 < args.length) {
166
+ envFilePath = args[i + 1];
167
+ i++;
168
+ } else if (args[i] === '--output' && i + 1 < args.length) {
169
+ outputFile = args[i + 1];
170
+ i++;
171
+ } else if (args[i] === '--type' && i + 1 < args.length) {
172
+ const type = args[i + 1];
173
+ if (type === 'abap' || type === 'xsuaa') {
174
+ authType = type;
175
+ } else {
176
+ console.error(`Invalid auth type: ${type}. Must be 'abap' or 'xsuaa'`);
177
+ process.exit(1);
178
+ }
179
+ i++;
180
+ } else if (args[i] === '--browser' && i + 1 < args.length) {
181
+ browser = args[i + 1];
182
+ if (!['none', 'chrome', 'edge', 'firefox', 'system', 'headless', 'auto'].includes(browser)) {
183
+ console.error(`Invalid browser: ${browser}. Must be one of: none, chrome, edge, firefox, system, headless, auto`);
184
+ process.exit(1);
185
+ }
186
+ i++;
187
+ } else if (args[i] === '--format' && i + 1 < args.length) {
188
+ const fmt = args[i + 1];
189
+ if (fmt === 'json' || fmt === 'env') {
190
+ format = fmt;
191
+ } else {
192
+ console.error(`Invalid format: ${fmt}. Must be 'json' or 'env'`);
193
+ process.exit(1);
194
+ }
195
+ i++;
196
+ } else if (args[i] === '--service-url' && i + 1 < args.length) {
197
+ serviceUrl = args[i + 1];
198
+ i++;
199
+ } else if (args[i] === '--redirect-port' && i + 1 < args.length) {
200
+ const port = parseInt(args[i + 1], 10);
201
+ if (isNaN(port) || port < 1 || port > 65535) {
202
+ console.error(`Invalid redirect port: ${args[i + 1]}. Must be a number between 1 and 65535`);
203
+ process.exit(1);
204
+ }
205
+ redirectPort = port;
206
+ i++;
207
+ } else {
208
+ console.error(`Unknown option: ${args[i]}`);
209
+ console.error('Run "mcp-auth --help" for usage information');
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ // Validate required arguments
215
+ if (!outputFile) {
216
+ console.error('Error: --output is required');
217
+ console.error('');
218
+ console.error('Usage: mcp-auth --output <path> [--service-key <path> | --env <path>] [options]');
219
+ console.error('Run "mcp-auth --help" for more information');
220
+ process.exit(1);
221
+ }
222
+
223
+ // Either service-key or env must be provided
224
+ if (!serviceKeyPath && !envFilePath) {
225
+ console.error('Error: Either --service-key or --env must be provided');
226
+ console.error('');
227
+ console.error('Usage: mcp-auth --output <path> [--service-key <path> | --env <path>] [options]');
228
+ console.error('Run "mcp-auth --help" for more information');
229
+ process.exit(1);
230
+ }
231
+
232
+ return {
233
+ serviceKeyPath,
234
+ envFilePath,
235
+ outputFile,
236
+ authType,
237
+ browser,
238
+ format,
239
+ serviceUrl,
240
+ redirectPort,
241
+ };
242
+ }
243
+
244
+ type ProviderMode = 'authorization_code' | 'client_credentials';
245
+
246
+ class BrokerTokenProvider implements ITokenProvider {
247
+ private mode: ProviderMode;
248
+ private browser?: string;
249
+ private redirectPort?: number;
250
+
251
+ constructor(mode: ProviderMode, browser?: string, redirectPort?: number) {
252
+ this.mode = mode;
253
+ this.browser = browser;
254
+ this.redirectPort = redirectPort;
255
+ }
256
+
257
+ async getConnectionConfig(
258
+ authConfig: IAuthorizationConfig,
259
+ options?: ITokenProviderOptions,
260
+ ): Promise<ITokenProviderResult> {
261
+ return this.getTokenResult(authConfig, options);
262
+ }
263
+
264
+ async refreshTokenFromSession(
265
+ authConfig: IAuthorizationConfig,
266
+ options?: ITokenProviderOptions,
267
+ ): Promise<ITokenProviderResult> {
268
+ return this.getTokenResult(authConfig, options);
269
+ }
270
+
271
+ async refreshTokenFromServiceKey(
272
+ authConfig: IAuthorizationConfig,
273
+ options?: ITokenProviderOptions,
274
+ ): Promise<ITokenProviderResult> {
275
+ return this.getTokenResult(authConfig, options);
276
+ }
277
+
278
+ private async getTokenResult(
279
+ authConfig: IAuthorizationConfig,
280
+ options?: ITokenProviderOptions,
281
+ ): Promise<ITokenProviderResult> {
282
+ const uaaUrl = authConfig.uaaUrl;
283
+ const uaaClientId = authConfig.uaaClientId;
284
+ const uaaClientSecret = authConfig.uaaClientSecret;
285
+
286
+ if (!uaaUrl || !uaaClientId || !uaaClientSecret) {
287
+ throw new Error('Auth config missing required UAA credentials');
288
+ }
289
+
290
+ if (this.mode === 'client_credentials') {
291
+ const provider = new ClientCredentialsProvider({
292
+ uaaUrl,
293
+ clientId: uaaClientId,
294
+ clientSecret: uaaClientSecret,
295
+ });
296
+ const result = await provider.getTokens();
297
+ return {
298
+ connectionConfig: {
299
+ authorizationToken: result.authorizationToken,
300
+ },
301
+ refreshToken: result.refreshToken,
302
+ };
303
+ }
304
+
305
+ const browserValue = options?.browser ?? this.browser ?? 'system';
306
+ const provider = new AuthorizationCodeProvider({
307
+ uaaUrl,
308
+ clientId: uaaClientId,
309
+ clientSecret: uaaClientSecret,
310
+ refreshToken: authConfig.refreshToken,
311
+ browser: browserValue,
312
+ redirectPort: this.redirectPort,
313
+ });
314
+ const result = await provider.getTokens();
315
+ return {
316
+ connectionConfig: {
317
+ authorizationToken: result.authorizationToken,
318
+ },
319
+ refreshToken: result.refreshToken,
320
+ };
321
+ }
322
+ }
323
+
324
+ function writeEnvFile(
325
+ outputPath: string,
326
+ authType: 'abap' | 'xsuaa',
327
+ token: string,
328
+ refreshToken?: string,
329
+ serviceUrl?: string,
330
+ uaaUrl?: string,
331
+ uaaClientId?: string,
332
+ uaaClientSecret?: string,
333
+ ): void {
334
+ const lines: string[] = [];
335
+
336
+ if (authType === 'abap') {
337
+ // ABAP format
338
+ if (serviceUrl) {
339
+ lines.push(`${ABAP_CONNECTION_VARS.SERVICE_URL}=${serviceUrl}`);
340
+ }
341
+ lines.push(`${ABAP_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token}`);
342
+
343
+ if (refreshToken) {
344
+ lines.push(`${ABAP_AUTHORIZATION_VARS.REFRESH_TOKEN}=${refreshToken}`);
345
+ }
346
+
347
+ if (uaaUrl) {
348
+ lines.push(`${ABAP_AUTHORIZATION_VARS.UAA_URL}=${uaaUrl}`);
349
+ }
350
+
351
+ if (uaaClientId) {
352
+ lines.push(`${ABAP_AUTHORIZATION_VARS.UAA_CLIENT_ID}=${uaaClientId}`);
353
+ }
354
+
355
+ if (uaaClientSecret) {
356
+ lines.push(`${ABAP_AUTHORIZATION_VARS.UAA_CLIENT_SECRET}=${uaaClientSecret}`);
357
+ }
358
+ } else {
359
+ // XSUAA format
360
+ if (serviceUrl) {
361
+ lines.push(`XSUAA_MCP_URL=${serviceUrl}`);
362
+ }
363
+ lines.push(`${XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token}`);
364
+
365
+ if (refreshToken) {
366
+ lines.push(`${XSUAA_AUTHORIZATION_VARS.REFRESH_TOKEN}=${refreshToken}`);
367
+ }
368
+
369
+ if (uaaUrl) {
370
+ lines.push(`${XSUAA_AUTHORIZATION_VARS.UAA_URL}=${uaaUrl}`);
371
+ }
372
+
373
+ if (uaaClientId) {
374
+ lines.push(`${XSUAA_AUTHORIZATION_VARS.UAA_CLIENT_ID}=${uaaClientId}`);
375
+ }
376
+
377
+ if (uaaClientSecret) {
378
+ lines.push(`${XSUAA_AUTHORIZATION_VARS.UAA_CLIENT_SECRET}=${uaaClientSecret}`);
379
+ }
380
+ }
381
+
382
+ // Ensure output directory exists
383
+ const outputDir = path.dirname(outputPath);
384
+ if (!fs.existsSync(outputDir)) {
385
+ fs.mkdirSync(outputDir, { recursive: true });
386
+ }
387
+
388
+ // Write to file
389
+ fs.writeFileSync(outputPath, lines.join('\n') + '\n', 'utf8');
390
+ }
391
+
392
+ function writeJsonFile(
393
+ outputPath: string,
394
+ token: string,
395
+ refreshToken?: string,
396
+ serviceUrl?: string,
397
+ uaaUrl?: string,
398
+ uaaClientId?: string,
399
+ uaaClientSecret?: string,
400
+ ): void {
401
+ const outputData: Record<string, string> = {
402
+ accessToken: token,
403
+ };
404
+
405
+ if (refreshToken) {
406
+ outputData.refreshToken = refreshToken;
407
+ }
408
+
409
+ if (serviceUrl) {
410
+ outputData.serviceUrl = serviceUrl;
411
+ }
412
+
413
+ if (uaaUrl) {
414
+ outputData.uaaUrl = uaaUrl;
415
+ }
416
+
417
+ if (uaaClientId) {
418
+ outputData.uaaClientId = uaaClientId;
419
+ }
420
+
421
+ if (uaaClientSecret) {
422
+ outputData.uaaClientSecret = uaaClientSecret;
423
+ }
424
+
425
+ // Ensure output directory exists
426
+ const outputDir = path.dirname(outputPath);
427
+ if (!fs.existsSync(outputDir)) {
428
+ fs.mkdirSync(outputDir, { recursive: true });
429
+ }
430
+
431
+ // Write to file
432
+ fs.writeFileSync(outputPath, JSON.stringify(outputData, null, 2), 'utf8');
433
+ }
434
+
435
+ async function main() {
436
+ const options = parseArgs();
437
+
438
+ if (!options) {
439
+ // Help or version was shown, exit already handled
440
+ return;
441
+ }
442
+
443
+ // Resolve paths
444
+ const resolvedOutputPath = path.resolve(options.outputFile);
445
+ const resolvedEnvPath = options.envFilePath
446
+ ? path.resolve(options.envFilePath)
447
+ : undefined;
448
+
449
+ // If service key is provided, use it; optionally read session from env
450
+ let serviceKeyStore: any = null;
451
+ let destination: string | undefined;
452
+ const envExists = resolvedEnvPath ? fs.existsSync(resolvedEnvPath) : false;
453
+
454
+ if (resolvedEnvPath) {
455
+ destination = path.basename(resolvedEnvPath, path.extname(resolvedEnvPath));
456
+ }
457
+
458
+ if (options.serviceKeyPath) {
459
+ const resolvedServiceKeyPath = path.resolve(options.serviceKeyPath);
460
+ const serviceKeyDir = path.dirname(resolvedServiceKeyPath);
461
+
462
+ // Check if service key file exists
463
+ if (!fs.existsSync(resolvedServiceKeyPath)) {
464
+ console.error(`❌ Service key file not found: ${resolvedServiceKeyPath}`);
465
+ process.exit(1);
466
+ }
467
+
468
+ const serviceKeyFileName = path.basename(resolvedServiceKeyPath, '.json');
469
+ if (destination && destination !== serviceKeyFileName) {
470
+ console.error(
471
+ `❌ Destination mismatch: env file (${destination}) vs service key (${serviceKeyFileName})`,
472
+ );
473
+ process.exit(1);
474
+ }
475
+ destination = serviceKeyFileName;
476
+
477
+ // Create appropriate stores based on auth type
478
+ serviceKeyStore = options.authType === 'xsuaa'
479
+ ? new XsuaaServiceKeyStore(serviceKeyDir)
480
+ : new AbapServiceKeyStore(serviceKeyDir);
481
+ }
482
+
483
+ if (!destination) {
484
+ console.error('❌ Destination could not be determined from inputs');
485
+ process.exit(1);
486
+ }
487
+
488
+ console.log(`📁 Output file: ${resolvedOutputPath}`);
489
+ if (resolvedEnvPath) {
490
+ console.log(`📁 Env file: ${resolvedEnvPath} (${envExists ? 'found' : 'not found'})`);
491
+ }
492
+ if (options.serviceKeyPath) {
493
+ console.log(`📁 Service key: ${path.resolve(options.serviceKeyPath)}`);
494
+ }
495
+ console.log(`🔐 Auth type: ${options.authType}`);
496
+ console.log(`🌐 Browser: ${options.browser || 'none (client_credentials)'}`);
497
+ console.log(`📄 Format: ${options.format}`);
498
+ if (options.serviceUrl) {
499
+ console.log(`🔗 Service URL: ${options.serviceUrl}`);
500
+ }
501
+
502
+ try {
503
+ if (!envExists && !serviceKeyStore) {
504
+ throw new Error('Env file not found and no service key provided.');
505
+ }
506
+
507
+ // Create temporary session store (work off a temp copy of env file)
508
+ const tempSessionDir = path.join(path.dirname(resolvedOutputPath), '.tmp');
509
+ if (!fs.existsSync(tempSessionDir)) {
510
+ fs.mkdirSync(tempSessionDir, { recursive: true });
511
+ }
512
+ if (envExists && resolvedEnvPath) {
513
+ const tempEnvPath = path.join(tempSessionDir, `${destination}.env`);
514
+ fs.copyFileSync(resolvedEnvPath, tempEnvPath);
515
+ }
516
+
517
+ // Resolve serviceUrl from service key if not provided explicitly
518
+ let actualServiceUrl = options.serviceUrl;
519
+ if (!actualServiceUrl && serviceKeyStore) {
520
+ const serviceKeyConn = await serviceKeyStore.getConnectionConfig(
521
+ destination,
522
+ );
523
+ actualServiceUrl = serviceKeyConn?.serviceUrl;
524
+ }
525
+
526
+ // For XSUAA, serviceUrl is optional - use placeholder only for AuthBroker internal work
527
+ const brokerServiceUrl = actualServiceUrl || '<SERVICE_URL>';
528
+
529
+ const sessionStore =
530
+ options.authType === 'xsuaa'
531
+ ? new XsuaaSessionStore(tempSessionDir, brokerServiceUrl)
532
+ : new AbapSessionStore(tempSessionDir);
533
+
534
+ const useBrowserAuth = options.browser !== undefined;
535
+ const providerMode: ProviderMode = useBrowserAuth
536
+ ? 'authorization_code'
537
+ : 'client_credentials';
538
+ const tokenProvider = new BrokerTokenProvider(
539
+ providerMode,
540
+ options.browser,
541
+ options.redirectPort,
542
+ );
543
+
544
+ const broker = new AuthBroker(
545
+ {
546
+ sessionStore,
547
+ serviceKeyStore: serviceKeyStore || undefined,
548
+ tokenProvider,
549
+ },
550
+ options.browser,
551
+ );
552
+
553
+ console.log(`🔐 Getting token for destination "${destination}"...`);
554
+ const token = await broker.getToken(destination);
555
+ console.log(`✅ Token obtained successfully`);
556
+
557
+ const connConfig = await sessionStore.getConnectionConfig(destination);
558
+ const authConfig = await sessionStore.getAuthorizationConfig(destination);
559
+
560
+ if (!token) {
561
+ throw new Error(
562
+ `Token provider did not return authorization token for destination "${destination}"`,
563
+ );
564
+ }
565
+
566
+ const outputServiceUrl =
567
+ options.serviceUrl || connConfig?.serviceUrl || actualServiceUrl;
568
+
569
+ // Write output file based on format
570
+ if (options.format === 'env') {
571
+ writeEnvFile(
572
+ resolvedOutputPath,
573
+ options.authType,
574
+ token,
575
+ authConfig?.refreshToken,
576
+ outputServiceUrl,
577
+ authConfig?.uaaUrl,
578
+ authConfig?.uaaClientId,
579
+ authConfig?.uaaClientSecret,
580
+ );
581
+
582
+ console.log(`✅ .env file created: ${resolvedOutputPath}`);
583
+
584
+ // Show what was written
585
+ console.log(`📋 .env file contains:`);
586
+ if (options.authType === 'abap') {
587
+ if (outputServiceUrl) {
588
+ console.log(` - ${ABAP_CONNECTION_VARS.SERVICE_URL}=${outputServiceUrl}`);
589
+ }
590
+ console.log(
591
+ ` - ${ABAP_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`,
592
+ );
593
+ if (authConfig?.refreshToken) {
594
+ console.log(
595
+ ` - ${ABAP_AUTHORIZATION_VARS.REFRESH_TOKEN}=${authConfig.refreshToken.substring(0, 50)}...`,
596
+ );
597
+ }
598
+ } else {
599
+ if (outputServiceUrl) {
600
+ console.log(` - XSUAA_MCP_URL=${outputServiceUrl}`);
601
+ }
602
+ console.log(
603
+ ` - ${XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`,
604
+ );
605
+ if (authConfig?.refreshToken) {
606
+ console.log(
607
+ ` - ${XSUAA_AUTHORIZATION_VARS.REFRESH_TOKEN}=${authConfig.refreshToken.substring(0, 50)}...`,
608
+ );
609
+ }
610
+ }
611
+ } else {
612
+ writeJsonFile(
613
+ resolvedOutputPath,
614
+ token,
615
+ authConfig?.refreshToken,
616
+ outputServiceUrl,
617
+ authConfig?.uaaUrl,
618
+ authConfig?.uaaClientId,
619
+ authConfig?.uaaClientSecret,
620
+ );
621
+
622
+ console.log(`✅ JSON file created: ${resolvedOutputPath}`);
623
+ console.log(`📋 Output contains:`);
624
+ console.log(` - accessToken: ${token.substring(0, 50)}...`);
625
+ if (authConfig?.refreshToken) {
626
+ console.log(
627
+ ` - refreshToken: ${authConfig.refreshToken.substring(0, 50)}...`,
628
+ );
629
+ }
630
+ if (outputServiceUrl) {
631
+ console.log(` - serviceUrl: ${outputServiceUrl}`);
632
+ }
633
+ }
634
+
635
+ // Cleanup temp directory
636
+ try {
637
+ fs.rmSync(tempSessionDir, { recursive: true, force: true });
638
+ } catch {
639
+ // Ignore cleanup errors
640
+ }
641
+
642
+ } catch (error: any) {
643
+ console.error(`❌ Error: ${error.message}`);
644
+ if (error.stack) {
645
+ console.error(error.stack);
646
+ }
647
+ process.exit(1);
648
+ }
649
+ }
650
+
651
+ main().catch((error) => {
652
+ console.error('Fatal error:', error);
653
+ process.exit(1);
654
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACd,MAAM,qBAAqB,CAAC;AA4C7B;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,YAAY,EAAE,aAAa,CAAC;IAC5B,uEAAuE;IACvE,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,4IAA4I;IAC5I,aAAa,EAAE,cAAc,CAAC;IAC9B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAiB;IACtC,OAAO,CAAC,gBAAgB,CAAU;IAElC;;;;;;;;;;;OAWG;gBACS,MAAM,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO;IAoFxE;;OAEG;YACW,eAAe;IA0D7B;;OAEG;YACW,aAAa;IAoD3B;;OAEG;YACW,iBAAiB;IA2D/B;;OAEG;YACW,kBAAkB;IAkChC;;OAEG;YACW,+BAA+B;IAyG7C;;OAEG;YACW,qBAAqB;IA8BnC;;OAEG;YACW,uBAAuB;IAwFrC;;OAEG;YACW,0BAA0B;IA8GxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACG,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA8GpD;;;;;OAKG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASxD;;;;OAIG;IACG,sBAAsB,CAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAoEvC;;;;OAIG;IACG,mBAAmB,CACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IA8DpC;;;;;;;;;;;;;;;;OAgBG;IACH,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe;CAqB3D"}
1
+ {"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACd,MAAM,qBAAqB,CAAC;AA4C7B;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,YAAY,EAAE,aAAa,CAAC;IAC5B,uEAAuE;IACvE,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,4IAA4I;IAC5I,aAAa,EAAE,cAAc,CAAC;IAC9B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAiB;IACtC,OAAO,CAAC,gBAAgB,CAAU;IAElC;;;;;;;;;;;OAWG;gBACS,MAAM,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO;IAoFxE;;OAEG;YACW,eAAe;IA0D7B;;OAEG;YACW,aAAa;IAoD3B;;OAEG;YACW,iBAAiB;IA2D/B;;OAEG;YACW,kBAAkB;IAkChC;;OAEG;YACW,+BAA+B;IA4G7C;;OAEG;YACW,qBAAqB;IA8BnC;;OAEG;YACW,uBAAuB;IAyFrC;;OAEG;YACW,0BAA0B;IAgHxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACG,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA8GpD;;;;;OAKG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASxD;;;;OAIG;IACG,sBAAsB,CAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAoEvC;;;;OAIG;IACG,mBAAmB,CACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IA8DpC;;;;;;;;;;;;;;;;OAgBG;IACH,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe;CAqB3D"}
@@ -276,9 +276,13 @@ class AuthBroker {
276
276
  throw error;
277
277
  }
278
278
  this.logger?.debug(`Step 0: Authenticating via provider (browser) for ${destination} using service key UAA credentials`);
279
+ const getConnectionConfig = this.tokenProvider.getConnectionConfig;
280
+ if (!getConnectionConfig) {
281
+ throw new Error('AuthBroker: tokenProvider.getConnectionConfig is required');
282
+ }
279
283
  let tokenResult;
280
284
  try {
281
- tokenResult = await this.tokenProvider.getConnectionConfig(serviceKeyAuthConfig, {
285
+ tokenResult = await getConnectionConfig(serviceKeyAuthConfig, {
282
286
  browser: this.browser,
283
287
  logger: this.logger,
284
288
  });
@@ -347,9 +351,13 @@ class AuthBroker {
347
351
  async refreshTokenFromSession(destination, uaaCredentials, refreshToken, serviceUrl) {
348
352
  this.logger?.debug(`Step 2a: Trying refreshTokenFromSession for ${destination}`);
349
353
  const authConfigWithRefresh = { ...uaaCredentials, refreshToken };
354
+ const refreshTokenFromSession = this.tokenProvider.refreshTokenFromSession;
355
+ if (!refreshTokenFromSession) {
356
+ throw new Error('AuthBroker: tokenProvider.refreshTokenFromSession is required');
357
+ }
350
358
  let tokenResult;
351
359
  try {
352
- tokenResult = await this.tokenProvider.refreshTokenFromSession(authConfigWithRefresh, {
360
+ tokenResult = await refreshTokenFromSession(authConfigWithRefresh, {
353
361
  browser: this.browser,
354
362
  logger: this.logger,
355
363
  });
@@ -408,9 +416,13 @@ class AuthBroker {
408
416
  throw error;
409
417
  }
410
418
  this.logger?.debug(`Step 2b: Trying refreshTokenFromServiceKey for ${destination}`);
419
+ const refreshTokenFromServiceKey = this.tokenProvider.refreshTokenFromServiceKey;
420
+ if (!refreshTokenFromServiceKey) {
421
+ throw new Error('AuthBroker: tokenProvider.refreshTokenFromServiceKey is required');
422
+ }
411
423
  let tokenResult;
412
424
  try {
413
- tokenResult = await this.tokenProvider.refreshTokenFromServiceKey(uaaCredentials, {
425
+ tokenResult = await refreshTokenFromServiceKey(uaaCredentials, {
414
426
  browser: this.browser,
415
427
  logger: this.logger,
416
428
  });
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-broker",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
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",
7
7
  "files": [
8
8
  "dist",
9
+ "bin",
9
10
  "README.md",
10
11
  "CHANGELOG.md",
11
12
  "CONTRIBUTORS.md",
@@ -49,19 +50,20 @@
49
50
  "generate-env": "tsx bin/generate-env-from-service-key.ts"
50
51
  },
51
52
  "bin": {
52
- "generate-env-from-service-key": "./bin/generate-env-from-service-key.ts"
53
+ "mcp-auth": "./bin/mcp-auth.js"
53
54
  },
54
55
  "engines": {
55
56
  "node": ">=18.0.0"
56
57
  },
57
58
  "dependencies": {
58
- "@mcp-abap-adt/interfaces": "^0.2.5",
59
- "axios": "^1.13.2"
59
+ "@mcp-abap-adt/auth-providers": "^0.2.6",
60
+ "@mcp-abap-adt/auth-stores": "^0.2.8",
61
+ "@mcp-abap-adt/interfaces": "^0.2.9",
62
+ "axios": "^1.13.2",
63
+ "tsx": "^4.21.0"
60
64
  },
61
65
  "devDependencies": {
62
66
  "@biomejs/biome": "^2.3.10",
63
- "@mcp-abap-adt/auth-providers": "^0.2.4",
64
- "@mcp-abap-adt/auth-stores": "^0.2.8",
65
67
  "@types/express": "^5.0.5",
66
68
  "@types/jest": "^30.0.0",
67
69
  "@types/js-yaml": "^4.0.9",
@@ -70,7 +72,6 @@
70
72
  "jest-util": "^30.2.0",
71
73
  "js-yaml": "^4.1.1",
72
74
  "ts-jest": "^29.2.5",
73
- "tsx": "^4.21.0",
74
75
  "typescript": "^5.9.2"
75
76
  }
76
77
  }