11#!/usr/bin/env node
2- import { OAuthStateMachine } from "../client/src/lib/oauth-state-machine.js" ;
3- import { EMPTY_DEBUGGER_STATE } from "../client/src/lib/auth-types.js" ;
2+ import { OAuthStateMachine , oauthTransitions } from "../client/src/lib/oauth-state-machine.js" ;
3+ import { EMPTY_DEBUGGER_STATE , AuthDebuggerState , OAuthStep } from "../client/src/lib/auth-types.js" ;
44import { DebugInspectorOAuthClientProvider , StorageInterface } from "../client/src/lib/auth.js" ;
5+ import http from "node:http" ;
6+ import https from "node:https" ;
7+ import readline from "node:readline" ;
58
69// Simple in-memory storage implementation for CLI usage
710class MemoryStorage implements StorageInterface {
11+ private storage : Map < string , string > ;
12+
813 constructor ( ) {
914 this . storage = new Map ( ) ;
1015 }
1116
12- getItem ( key ) {
17+ getItem ( key : string ) : string | null {
1318 return this . storage . get ( key ) || null ;
1419 }
1520
16- setItem ( key , value ) {
21+ setItem ( key : string , value : string ) : void {
1722 this . storage . set ( key , value ) ;
1823 }
1924
20- removeItem ( key ) {
25+ removeItem ( key : string ) : void {
2126 this . storage . delete ( key ) ;
2227 }
2328}
2429
2530class CLIAuthFlowTester {
26- constructor ( serverUrl ) {
31+ serverUrl : string ;
32+ state : AuthDebuggerState ;
33+ storage : MemoryStorage ;
34+ stateMachine : OAuthStateMachine ;
35+ autoRedirect : boolean ;
36+
37+ constructor ( serverUrl : string , autoRedirect : boolean = false ) {
2738 this . serverUrl = serverUrl ;
2839 this . state = { ...EMPTY_DEBUGGER_STATE } ;
2940 this . storage = new MemoryStorage ( ) ;
3041 this . stateMachine = new OAuthStateMachine ( serverUrl , this . updateState . bind ( this ) ) ;
42+ this . autoRedirect = autoRedirect ;
3143 }
3244
33- updateState ( updates ) {
45+ updateState ( updates : Partial < AuthDebuggerState > ) {
3446 this . state = { ...this . state , ...updates } ;
3547 }
3648
37- log ( step , message , data = null ) {
49+ log ( step : string , message : string , data : any = null ) {
3850 const timestamp = new Date ( ) . toISOString ( ) ;
3951 console . log ( `[${ timestamp } ] ${ step } : ${ message } ` ) ;
4052 if ( data ) {
4153 console . log ( ` └─ Details:` , JSON . stringify ( data , null , 2 ) ) ;
4254 }
4355 }
4456
45- error ( step , message , error = null ) {
57+ error ( step : string , message : string , error : Error | null = null ) {
4658 const timestamp = new Date ( ) . toISOString ( ) ;
4759 console . error ( `[${ timestamp } ] ❌ ${ step } : ${ message } ` ) ;
4860 if ( error ) {
4961 console . error ( ` └─ Error:` , error . message ) ;
5062 }
5163 }
5264
53- success ( step , message , data = null ) {
65+ success ( step : string , message : string , data : any = null ) {
5466 const timestamp = new Date ( ) . toISOString ( ) ;
5567 console . log ( `[${ timestamp } ] ✅ ${ step } : ${ message } ` ) ;
5668 if ( data ) {
5769 console . log ( ` └─ Data:` , JSON . stringify ( data , null , 2 ) ) ;
5870 }
5971 }
6072
61- async executeStep ( stepName ) {
73+ async executeStep ( stepName : OAuthStep ) {
6274 this . log ( stepName . toUpperCase ( ) . replace ( '_' , ' ' ) , `Executing ${ stepName } ...` ) ;
63-
75+
6476 try {
6577 // Override the provider creation for CLI usage
6678 const originalExecuteStep = this . stateMachine . executeStep ;
67- this . stateMachine . executeStep = async ( state ) => {
79+ this . stateMachine . executeStep = async ( state : AuthDebuggerState ) => {
6880 // Create a provider with our memory storage
6981 const provider = new DebugInspectorOAuthClientProvider ( this . serverUrl , this . storage ) ;
70-
82+
7183 // Override redirectToAuthorization for CLI usage
72- provider . redirectToAuthorization = ( url ) => {
84+ provider . redirectToAuthorization = ( url : URL ) => {
7385 console . log ( `\nPlease open this authorization URL in your browser:` ) ;
7486 console . log ( `\n${ url } \n` ) ;
7587 } ;
76-
88+
7789 const context = {
7890 state,
7991 serverUrl : this . serverUrl ,
8092 provider,
8193 updateState : this . updateState . bind ( this ) ,
8294 } ;
83-
84- const transitions = ( await import ( "../client/src/lib/oauth-state-machine.js" ) ) . oauthTransitions ;
85- const transition = transitions [ state . oauthStep ] ;
86-
95+
96+ const transition = oauthTransitions [ state . oauthStep ] ;
97+
8798 if ( ! ( await transition . canTransition ( context ) ) ) {
8899 throw new Error ( `Cannot transition from ${ state . oauthStep } ` ) ;
89100 }
90-
101+
91102 await transition . execute ( context ) ;
92103 } ;
93-
104+
94105 await this . stateMachine . executeStep ( this . state ) ;
95-
106+
96107 // Restore original method
97108 this . stateMachine . executeStep = originalExecuteStep ;
98-
109+
99110 // Log step-specific success information
100111 switch ( stepName ) {
101112 case 'metadata_discovery' :
@@ -112,7 +123,7 @@ class CLIAuthFlowTester {
112123 resource_metadata_error : this . state . resourceMetadataError ?. message ,
113124 } ) ;
114125 break ;
115-
126+
116127 case 'client_registration' :
117128 this . success ( 'CLIENT REGISTRATION' , 'Client registered successfully' , {
118129 client_id : this . state . oauthClientInfo ?. client_id ,
@@ -121,17 +132,17 @@ class CLIAuthFlowTester {
121132 client_secret_expires_at : this . state . oauthClientInfo ?. client_secret_expires_at ,
122133 } ) ;
123134 break ;
124-
135+
125136 case 'authorization_redirect' :
126137 this . success ( 'AUTHORIZATION PREPARE' , 'Authorization URL generated' , {
127138 url : this . state . authorizationUrl ,
128139 } ) ;
129140 break ;
130-
141+
131142 case 'authorization_code' :
132143 this . success ( 'AUTHORIZATION CODE' , 'Authorization code validated' ) ;
133144 break ;
134-
145+
135146 case 'token_request' :
136147 this . success ( 'TOKEN EXCHANGE' , 'Tokens obtained successfully' , {
137148 access_token : this . state . oauthTokens ?. access_token ? '[REDACTED]' : undefined ,
@@ -142,7 +153,7 @@ class CLIAuthFlowTester {
142153 } ) ;
143154 break ;
144155 }
145-
156+
146157 return true ;
147158 } catch ( error ) {
148159 this . error ( stepName . toUpperCase ( ) . replace ( '_' , ' ' ) , `Failed to execute ${ stepName } ` , error ) ;
@@ -151,45 +162,116 @@ class CLIAuthFlowTester {
151162 }
152163 }
153164
154- async promptForAuthCode ( ) {
165+ async promptForAuthCode ( ) : Promise < string > {
166+ if ( this . autoRedirect ) {
167+ return this . fetchAuthCodeFromRedirect ( this . state . authorizationUrl ! ) ;
168+ }
169+
155170 return new Promise ( ( resolve ) => {
156- const readline = require ( 'readline' ) ;
157171 const rl = readline . createInterface ( {
158172 input : process . stdin ,
159173 output : process . stdout
160174 } ) ;
161175
162- rl . question ( 'Enter the authorization code: ' , ( code ) => {
176+ rl . question ( 'Enter the authorization code: ' , ( code : string ) => {
163177 rl . close ( ) ;
164178 resolve ( code . trim ( ) ) ;
165179 } ) ;
166180 } ) ;
167181 }
168182
169- async runFullFlow ( ) {
183+ async fetchAuthCodeFromRedirect ( authUrl : string ) : Promise < string > {
184+ console . log ( `🔄 AUTO-REDIRECT: Fetching authorization URL...` ) ;
185+
186+ try {
187+ const url = new URL ( authUrl ) ;
188+
189+ return new Promise ( ( resolve , reject ) => {
190+ const protocol = url . protocol === 'https:' ? https : http ;
191+
192+ const req = protocol . get ( authUrl , {
193+ headers : { 'User-Agent' : 'MCP-Inspector/1.0' }
194+ } , ( res ) => {
195+ // Check if we got a redirect
196+ if ( res . statusCode >= 300 && res . statusCode < 400 && res . headers . location ) {
197+ const redirectUrl = new URL ( res . headers . location , authUrl ) ;
198+ console . log ( `🔄 Found redirect to: ${ redirectUrl } ` ) ;
199+
200+ // Check if the redirect URL has a code
201+ const code = redirectUrl . searchParams . get ( 'code' ) ;
202+ if ( code ) {
203+ console . log ( `✅ Successfully extracted authorization code from redirect` ) ;
204+ resolve ( code ) ;
205+ return ;
206+ }
207+
208+ reject ( new Error ( 'No authorization code found in the redirect URL' ) ) ;
209+ } else {
210+ reject ( new Error ( `Expected redirect, got status code ${ res . statusCode } ` ) ) ;
211+ }
212+ } ) ;
213+
214+ req . on ( 'error' , ( error ) => {
215+ reject ( error ) ;
216+ } ) ;
217+
218+ req . end ( ) ;
219+ } ) ;
220+ } catch ( error ) {
221+ console . error ( `❌ Error fetching authorization URL: ${ error . message } ` ) ;
222+
223+ // Fallback to manual entry if auto-redirect fails
224+ console . log ( "\n⚠️ Auto-redirect failed. Please manually authorize and enter the code." ) ;
225+ console . log ( `Please open this URL in your browser: ${ authUrl } \n` ) ;
226+
227+ return new Promise ( ( resolve ) => {
228+ const rl = readline . createInterface ( {
229+ input : process . stdin ,
230+ output : process . stdout
231+ } ) ;
232+
233+ rl . question ( 'Enter the authorization code: ' , ( code : string ) => {
234+ rl . close ( ) ;
235+ resolve ( code . trim ( ) ) ;
236+ } ) ;
237+ } ) ;
238+ }
239+ }
240+
241+ async runFullFlow ( ) : Promise < boolean > {
170242 console . log ( `\n🚀 Starting MCP Server Authorization Flow Test` ) ;
171- console . log ( `📡 Server URL: ${ this . serverUrl } \n` ) ;
243+ console . log ( `📡 Server URL: ${ this . serverUrl } ` ) ;
244+ if ( this . autoRedirect ) {
245+ console . log ( `🤖 Auto-redirect mode: Enabled\n` ) ;
246+ } else {
247+ console . log ( `👤 Manual authorization mode\n` ) ;
248+ }
172249
173250 try {
174251 // Step 1: Metadata Discovery
175252 await this . executeStep ( 'metadata_discovery' ) ;
176253 console . log ( "" ) ;
177254
178- // Step 2: Client Registration
255+ // Step 2: Client Registration
179256 await this . executeStep ( 'client_registration' ) ;
180257 console . log ( "" ) ;
181258
182259 // Step 3: Authorization Redirect Preparation
183260 await this . executeStep ( 'authorization_redirect' ) ;
184261 console . log ( "" ) ;
185262
186- // Step 4: Manual authorization step
187- console . log ( "🔐 AUTHORIZATION REQUIRED" ) ;
188- console . log ( "Please open the following URL in your browser:" ) ;
189- console . log ( `\n${ this . state . authorizationUrl } \n` ) ;
190- console . log ( "After authorizing, you will be redirected to a callback URL." ) ;
191- console . log ( "Copy the authorization code from the callback URL parameters.\n" ) ;
192-
263+ // Step 4: Authorization step (automatic or manual)
264+ if ( this . autoRedirect ) {
265+ console . log ( "🔐 AUTO-AUTHORIZATION" ) ;
266+ console . log ( "Automatically following the authorization URL and extracting the code...\n" ) ;
267+ } else {
268+ console . log ( "🔐 MANUAL AUTHORIZATION REQUIRED" ) ;
269+ console . log ( "Please open the following URL in your browser:" ) ;
270+ console . log ( `\n${ this . state . authorizationUrl } \n` ) ;
271+ console . log ( "After authorizing, you will be redirected to a callback URL." ) ;
272+ console . log ( "Copy the authorization code from the callback URL parameters.\n" ) ;
273+ }
274+
193275 const authCode = await this . promptForAuthCode ( ) ;
194276 this . state . authorizationCode = authCode ;
195277 console . log ( "" ) ;
@@ -206,33 +288,35 @@ class CLIAuthFlowTester {
206288 console . log ( "🎉 AUTHORIZATION FLOW COMPLETED SUCCESSFULLY!" ) ;
207289 console . log ( "✅ All steps completed without errors" ) ;
208290 console . log ( "🔑 Access token obtained and ready for use" ) ;
209-
291+
210292 return true ;
211293 } catch ( error ) {
212294 console . log ( "\n❌ AUTHORIZATION FLOW FAILED!" ) ;
213295 console . error ( "💥 Error:" , error . message ) ;
214-
296+
215297 if ( this . state . validationError ) {
216298 console . error ( "🔍 Validation Error:" , this . state . validationError ) ;
217299 }
218-
300+
219301 return false ;
220302 }
221303 }
222304}
223305
224306// Main execution
225- async function main ( ) {
307+ async function main ( ) : Promise < void > {
226308 const args = process . argv . slice ( 2 ) ;
227-
309+
228310 if ( args . length === 0 ) {
229- console . error ( "Usage: node test-auth-flow.js <server-url>" ) ;
230- console . error ( "Example: node test-auth-flow.js https://example.com/mcp" ) ;
311+ console . error ( "Usage: npx tsx test-auth-flow.ts <server-url> [--auto-redirect]" ) ;
312+ console . error ( "Example: npx tsx test-auth-flow.ts https://example.com/mcp" ) ;
313+ console . error ( "Add --auto-redirect flag for servers that redirect immediately without user interaction" ) ;
231314 process . exit ( 1 ) ;
232315 }
233316
234317 const serverUrl = args [ 0 ] ;
235-
318+ const autoRedirect = args . includes ( '--auto-redirect' ) ;
319+
236320 // Validate URL
237321 try {
238322 new URL ( serverUrl ) ;
@@ -241,18 +325,18 @@ async function main() {
241325 process . exit ( 1 ) ;
242326 }
243327
244- const tester = new CLIAuthFlowTester ( serverUrl ) ;
328+ const tester = new CLIAuthFlowTester ( serverUrl , autoRedirect ) ;
245329 const success = await tester . runFullFlow ( ) ;
246-
330+
247331 process . exit ( success ? 0 : 1 ) ;
248332}
249333
250334// Handle unhandled promise rejections
251- process . on ( 'unhandledRejection' , ( reason , promise ) => {
335+ process . on ( 'unhandledRejection' , ( reason : any , promise : Promise < any > ) => {
252336 console . error ( 'Unhandled Rejection at:' , promise , 'reason:' , reason ) ;
253337 process . exit ( 1 ) ;
254338} );
255339
256- if ( import . meta . url === `file:// ${ process . argv [ 1 ] } ` ) {
257- main ( ) ;
258- }
340+ // Direct execution check for ESM
341+ // When run with tsx, just call main directly
342+ main();
0 commit comments