#!/usr/bin/env node import { createServer } from 'node:http'; import { createInterface } from 'node:readline'; import { URL } from 'node:url'; import { exec } from 'node:child_process'; import { Client } from '../../client/index.js'; import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js'; import { OAuthClientMetadata } from '../../shared/auth.js'; import { CallToolRequest, ListToolsRequest, CallToolResultSchema, ListToolsResultSchema } from '../../types.js'; import { UnauthorizedError } from '../../client/auth.js'; import { InMemoryOAuthClientProvider } from './simpleOAuthClientProvider.js'; // Configuration const DEFAULT_SERVER_URL = 'http://localhost:3000/mcp'; const CALLBACK_PORT = 8090; // Use different port than auth server (3001) const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`; /** * Interactive MCP client with OAuth authentication * Demonstrates the complete OAuth flow with browser-based authorization */ class InteractiveOAuthClient { private client: Client | null = null; private readonly rl = createInterface({ input: process.stdin, output: process.stdout }); constructor( private serverUrl: string, private clientMetadataUrl?: string ) {} /** * Prompts user for input via readline */ private async question(query: string): Promise { return new Promise(resolve => { this.rl.question(query, resolve); }); } /** * Opens the authorization URL in the user's default browser */ private async openBrowser(url: string): Promise { console.log(`🌐 Opening browser for authorization: ${url}`); const command = `open "${url}"`; exec(command, error => { if (error) { console.error(`Failed to open browser: ${error.message}`); console.log(`Please manually open: ${url}`); } }); } /** * Example OAuth callback handler - in production, use a more robust approach * for handling callbacks and storing tokens */ /** * Starts a temporary HTTP server to receive the OAuth callback */ private async waitForOAuthCallback(): Promise { return new Promise((resolve, reject) => { const server = createServer((req, res) => { // Ignore favicon requests if (req.url === '/favicon.ico') { res.writeHead(404); res.end(); return; } console.log(`šŸ“„ Received callback: ${req.url}`); const parsedUrl = new URL(req.url || '', 'http://localhost'); const code = parsedUrl.searchParams.get('code'); const error = parsedUrl.searchParams.get('error'); if (code) { console.log(`āœ… Authorization code received: ${code?.substring(0, 10)}...`); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(`

Authorization Successful!

You can close this window and return to the terminal.

`); resolve(code); setTimeout(() => server.close(), 3000); } else if (error) { console.log(`āŒ Authorization error: ${error}`); res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(`

Authorization Failed

Error: ${error}

`); reject(new Error(`OAuth authorization failed: ${error}`)); } else { console.log(`āŒ No authorization code or error in callback`); res.writeHead(400); res.end('Bad request'); reject(new Error('No authorization code provided')); } }); server.listen(CALLBACK_PORT, () => { console.log(`OAuth callback server started on http://localhost:${CALLBACK_PORT}`); }); }); } private async attemptConnection(oauthProvider: InMemoryOAuthClientProvider): Promise { console.log('🚢 Creating transport with OAuth provider...'); const baseUrl = new URL(this.serverUrl); const transport = new StreamableHTTPClientTransport(baseUrl, { authProvider: oauthProvider }); console.log('🚢 Transport created'); try { console.log('šŸ”Œ Attempting connection (this will trigger OAuth redirect)...'); await this.client!.connect(transport); console.log('āœ… Connected successfully'); } catch (error) { if (error instanceof UnauthorizedError) { console.log('šŸ” OAuth required - waiting for authorization...'); const callbackPromise = this.waitForOAuthCallback(); const authCode = await callbackPromise; await transport.finishAuth(authCode); console.log('šŸ” Authorization code received:', authCode); console.log('šŸ”Œ Reconnecting with authenticated transport...'); await this.attemptConnection(oauthProvider); } else { console.error('āŒ Connection failed with non-auth error:', error); throw error; } } } /** * Establishes connection to the MCP server with OAuth authentication */ async connect(): Promise { console.log(`šŸ”— Attempting to connect to ${this.serverUrl}...`); const clientMetadata: OAuthClientMetadata = { client_name: 'Simple OAuth MCP Client', redirect_uris: [CALLBACK_URL], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'client_secret_post' }; console.log('šŸ” Creating OAuth provider...'); const oauthProvider = new InMemoryOAuthClientProvider( CALLBACK_URL, clientMetadata, (redirectUrl: URL) => { console.log(`šŸ“Œ OAuth redirect handler called - opening browser`); console.log(`Opening browser to: ${redirectUrl.toString()}`); this.openBrowser(redirectUrl.toString()); }, this.clientMetadataUrl ); console.log('šŸ” OAuth provider created'); console.log('šŸ‘¤ Creating MCP client...'); this.client = new Client( { name: 'simple-oauth-client', version: '1.0.0' }, { capabilities: {} } ); console.log('šŸ‘¤ Client created'); console.log('šŸ” Starting OAuth flow...'); await this.attemptConnection(oauthProvider); // Start interactive loop await this.interactiveLoop(); } /** * Main interactive loop for user commands */ async interactiveLoop(): Promise { console.log('\nšŸŽÆ Interactive MCP Client with OAuth'); console.log('Commands:'); console.log(' list - List available tools'); console.log(' call [args] - Call a tool'); console.log(' stream [args] - Call a tool with streaming (shows task status)'); console.log(' quit - Exit the client'); console.log(); while (true) { try { const command = await this.question('mcp> '); if (!command.trim()) { continue; } if (command === 'quit') { console.log('\nšŸ‘‹ Goodbye!'); this.close(); process.exit(0); } else if (command === 'list') { await this.listTools(); } else if (command.startsWith('call ')) { await this.handleCallTool(command); } else if (command.startsWith('stream ')) { await this.handleStreamTool(command); } else { console.log("āŒ Unknown command. Try 'list', 'call ', 'stream ', or 'quit'"); } } catch (error) { if (error instanceof Error && error.message === 'SIGINT') { console.log('\n\nšŸ‘‹ Goodbye!'); break; } console.error('āŒ Error:', error); } } } private async listTools(): Promise { if (!this.client) { console.log('āŒ Not connected to server'); return; } try { const request: ListToolsRequest = { method: 'tools/list', params: {} }; const result = await this.client.request(request, ListToolsResultSchema); if (result.tools && result.tools.length > 0) { console.log('\nšŸ“‹ Available tools:'); result.tools.forEach((tool, index) => { console.log(`${index + 1}. ${tool.name}`); if (tool.description) { console.log(` Description: ${tool.description}`); } console.log(); }); } else { console.log('No tools available'); } } catch (error) { console.error('āŒ Failed to list tools:', error); } } private async handleCallTool(command: string): Promise { const parts = command.split(/\s+/); const toolName = parts[1]; if (!toolName) { console.log('āŒ Please specify a tool name'); return; } // Parse arguments (simple JSON-like format) let toolArgs: Record = {}; if (parts.length > 2) { const argsString = parts.slice(2).join(' '); try { toolArgs = JSON.parse(argsString); } catch { console.log('āŒ Invalid arguments format (expected JSON)'); return; } } await this.callTool(toolName, toolArgs); } private async callTool(toolName: string, toolArgs: Record): Promise { if (!this.client) { console.log('āŒ Not connected to server'); return; } try { const request: CallToolRequest = { method: 'tools/call', params: { name: toolName, arguments: toolArgs } }; const result = await this.client.request(request, CallToolResultSchema); console.log(`\nšŸ”§ Tool '${toolName}' result:`); if (result.content) { result.content.forEach(content => { if (content.type === 'text') { console.log(content.text); } else { console.log(content); } }); } else { console.log(result); } } catch (error) { console.error(`āŒ Failed to call tool '${toolName}':`, error); } } private async handleStreamTool(command: string): Promise { const parts = command.split(/\s+/); const toolName = parts[1]; if (!toolName) { console.log('āŒ Please specify a tool name'); return; } // Parse arguments (simple JSON-like format) let toolArgs: Record = {}; if (parts.length > 2) { const argsString = parts.slice(2).join(' '); try { toolArgs = JSON.parse(argsString); } catch { console.log('āŒ Invalid arguments format (expected JSON)'); return; } } await this.streamTool(toolName, toolArgs); } private async streamTool(toolName: string, toolArgs: Record): Promise { if (!this.client) { console.log('āŒ Not connected to server'); return; } try { // Using the experimental tasks API - WARNING: may change without notice console.log(`\nšŸ”§ Streaming tool '${toolName}'...`); const stream = this.client.experimental.tasks.callToolStream( { name: toolName, arguments: toolArgs }, CallToolResultSchema, { task: { taskId: `task-${Date.now()}`, ttl: 60000 } } ); // Iterate through all messages yielded by the generator for await (const message of stream) { switch (message.type) { case 'taskCreated': console.log(`āœ“ Task created: ${message.task.taskId}`); break; case 'taskStatus': console.log(`⟳ Status: ${message.task.status}`); if (message.task.statusMessage) { console.log(` ${message.task.statusMessage}`); } break; case 'result': console.log('āœ“ Completed!'); message.result.content.forEach(content => { if (content.type === 'text') { console.log(content.text); } else { console.log(content); } }); break; case 'error': console.log('āœ— Error:'); console.log(` ${message.error.message}`); break; } } } catch (error) { console.error(`āŒ Failed to stream tool '${toolName}':`, error); } } close(): void { this.rl.close(); if (this.client) { // Note: Client doesn't have a close method in the current implementation // This would typically close the transport connection } } } /** * Main entry point */ async function main(): Promise { const args = process.argv.slice(2); const serverUrl = args[0] || DEFAULT_SERVER_URL; const clientMetadataUrl = args[1]; console.log('šŸš€ Simple MCP OAuth Client'); console.log(`Connecting to: ${serverUrl}`); if (clientMetadataUrl) { console.log(`Client Metadata URL: ${clientMetadataUrl}`); } console.log(); const client = new InteractiveOAuthClient(serverUrl, clientMetadataUrl); // Handle graceful shutdown process.on('SIGINT', () => { console.log('\n\nšŸ‘‹ Goodbye!'); client.close(); process.exit(0); }); try { await client.connect(); } catch (error) { console.error('Failed to start client:', error); process.exit(1); } finally { client.close(); } } // Run if this file is executed directly main().catch(error => { console.error('Unhandled error:', error); process.exit(1); });