Skip to content

Commit 60f6b68

Browse files
committed
tsify
1 parent 53f7558 commit 60f6b68

File tree

1 file changed

+140
-56
lines changed

1 file changed

+140
-56
lines changed
Lines changed: 140 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,112 @@
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";
44
import { 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
710
class 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

2530
class 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

Comments
 (0)