Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
test-oauth-full-flow.jsโ€ข17.8 kB
#!/usr/bin/env node /** * Full OAuth Flow Test Script for MCP Server * * This script: * 1. Connects to the MCP server * 2. Initiates OAuth flow and gets device code * 3. Opens browser for user authentication * 4. Waits for authentication completion * 5. Tests authenticated GitHub API access * 6. Retrieves repository data to verify OAuth works */ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { spawn, exec } from 'child_process'; import open from 'open'; import chalk from 'chalk'; import { promisify } from 'util'; import readline from 'readline'; import { isTestMode, getAuthToken, validateToken } from './utils/github-auth.js'; const execAsync = promisify(exec); /** * WARNING: OAuth Testing Mode Support * This script supports both PAT (testing) and OAuth device flow (production) * - PAT Mode: Set TEST_GITHUB_TOKEN environment variable * - OAuth Mode: Leave TEST_GITHUB_TOKEN unset * See docs/development/OAUTH_TESTING_VS_PRODUCTION.md for critical differences */ class MCPOAuthTester { constructor() { this.client = null; this.transport = null; } /** * Mask sensitive data for logging while preserving debugging value */ maskSensitive(str, type = 'default') { if (!str || typeof str !== 'string') return '***'; switch (type) { case 'tool': // For tool names: show first 3 chars and mask the rest if (str.length <= 3) return '***'; return str.substring(0, 3) + '*'.repeat(Math.min(str.length - 3, 10)); case 'url': // For URLs: mask domain but keep protocol and path structure try { const url = new URL(str); const maskedHost = url.hostname.substring(0, 3) + '*'.repeat(Math.min(url.hostname.length - 3, 8)); return `${url.protocol}//${maskedHost}${url.pathname}`; } catch { // Fallback if not a valid URL return this.maskSensitive(str, 'default'); } case 'token': // For tokens: show first 4 chars only if (str.length <= 4) return '****'; return str.substring(0, 4) + '*'.repeat(Math.min(str.length - 4, 16)); default: // General masking: show first 3 chars if (str.length <= 3) return '***'; return str.substring(0, 3) + '*'.repeat(Math.min(str.length - 3, 10)); } } /** * Sanitize error message while preserving useful debugging information */ sanitizeError(error, context = '') { if (!error) return new Error('Unknown error'); const sanitizedError = new Error(`${context ? context + ': ' : ''}${error.message ? this.maskSensitive(error.message) : 'Unknown error'}`); sanitizedError.code = error.code || 'UNKNOWN_ERROR'; sanitizedError.name = error.name || 'Error'; // Preserve non-sensitive properties if (error.status) sanitizedError.status = error.status; if (error.statusCode) sanitizedError.statusCode = error.statusCode; return sanitizedError; } /** * Create readline interface for user input */ createReadlineInterface() { return readline.createInterface({ input: process.stdin, output: process.stdout }); } /** * Ask user a yes/no question */ async askYesNo(question) { const rl = this.createReadlineInterface(); return new Promise((resolve) => { rl.question(`${question} (y/n): `, (answer) => { rl.close(); resolve(answer.toLowerCase() === 'y'); }); }); } /** * Wait for user to press Enter */ async waitForEnter(message = 'Press Enter to continue...') { const rl = this.createReadlineInterface(); return new Promise((resolve) => { rl.question(message, () => { rl.close(); resolve(); }); }); } /** * Connect to the MCP server */ async connectToMCP() { console.log(chalk.blue('๐Ÿ”„ Connecting to MCP server...')); try { // Create transport and client (transport will spawn the process) this.transport = new StdioClientTransport({ command: 'node', args: ['dist/index.js'], cwd: process.cwd() }); this.client = new Client({ name: 'oauth-test-client', version: '1.0.0' }, { capabilities: {} }); await this.client.connect(this.transport); console.log(chalk.green('โœ… Connected to MCP server')); // Discover available tools const tools = await this.client.listTools(); console.log(chalk.blue(`\n๐Ÿ“‹ Discovered ${tools.tools.length} available tools`)); // Check for OAuth tools const oauthTools = tools.tools.filter(tool => tool.name.includes('auth') || tool.name.includes('oauth') ); if (oauthTools.length === 0) { throw new Error('No OAuth tools found in MCP server'); } // Use a safe, untainted count variable const toolCount = Number(oauthTools.length); // Log only the count - no iteration over tainted data if (toolCount > 0) { console.log(chalk.green(`โœ… OAuth authentication tools detected (${toolCount} available)`)); } else { console.log(chalk.red('โŒ No OAuth tools detected')); } return true; } catch (error) { console.log(chalk.red('โŒ Failed to connect to MCP server')); // Sanitize error before rethrowing to avoid leaking sensitive information const sanitizedError = this.sanitizeError(error, 'MCP Connection Failed'); throw sanitizedError; } } /** * Call an MCP tool */ async callTool(toolName, args = {}) { try { const result = await this.client.callTool({ name: toolName, arguments: args }); return result; } catch (error) { console.error(chalk.red(`Error calling tool ${this.maskSensitive(toolName, 'tool')}:`), this.maskSensitive(error.message || 'Unknown error')); throw this.sanitizeError(error, 'Tool Call Failed'); } } /** * Start OAuth authentication flow */ async startOAuthFlow() { // WARNING: This code path differs for TESTING (PAT) vs PRODUCTION (OAuth) // See docs/development/OAUTH_TESTING_VS_PRODUCTION.md if (isTestMode()) { console.log(chalk.blue('\n๐Ÿงช TEST MODE: Using Personal Access Token\n')); console.log(chalk.yellow('โš ๏ธ Skipping OAuth device flow (used in production)')); console.log(chalk.yellow(' To test the real OAuth flow, unset TEST_GITHUB_TOKEN')); const token = await getAuthToken(); const validation = await validateToken(token); if (!validation.valid) { console.error(chalk.red('โŒ PAT validation failed:'), validation.error); return false; } console.log(chalk.green('โœ… PAT authenticated as:'), validation.user); console.log(chalk.gray(' Scopes:'), validation.scopes.join(', ')); console.log(chalk.green('\nโœ… Authentication ready for testing!')); return true; } console.log(chalk.yellow('\n๐Ÿ” Starting OAuth Authentication Flow...\n')); // First check current auth status console.log(chalk.blue('๐Ÿ”„ Checking current authentication status...')); const authStatus = await this.callTool('check_github_auth'); console.log(chalk.green('โœ… Status check complete')); if (authStatus.content?.[0]?.text?.includes('โœ…')) { console.log(chalk.green('โœ… Already authenticated with GitHub!')); return true; } // Start OAuth setup console.log(chalk.blue('๐Ÿ”„ Initiating GitHub OAuth...')); const setupResult = await this.callTool('setup_github_auth'); console.log(chalk.green('โœ… OAuth initiated')); // Extract device code and verification URL const resultText = setupResult.content?.[0]?.text || ''; const codeMatch = resultText.match(/Enter code:\s*\*\*([A-Z0-9]{4}-[A-Z0-9]{4})\*\*/) || resultText.match(/code:\s*\*\*([A-Z0-9]{4}-[A-Z0-9]{4})\*\*/i); const urlMatch = resultText.match(/https:\/\/github\.com\/login\/device/); if (!codeMatch || !urlMatch) { console.error(chalk.red('Failed to extract device code from response')); console.log('Response:', resultText); return false; } const deviceCode = codeMatch[1]; const verificationUrl = 'https://github.com/login/device'; console.log(chalk.cyan('\n' + '='.repeat(60))); console.log(chalk.bold.white('GitHub Device Authentication Required')); console.log(chalk.cyan('='.repeat(60))); console.log(chalk.yellow(`\n๐Ÿ”‘ User Code: ${chalk.bold.white(deviceCode)}`)); console.log(chalk.yellow(`๐ŸŒ Verification URL: ${chalk.bold.white(verificationUrl)}`)); console.log(chalk.cyan('\n' + '='.repeat(60) + '\n')); // Ask if user wants to open browser automatically const shouldOpenBrowser = await this.askYesNo('Open browser automatically?'); if (shouldOpenBrowser) { console.log(chalk.blue('Opening browser...')); await open(verificationUrl); console.log(chalk.green('โœ… Browser opened')); } else { console.log(chalk.yellow(`\nPlease open your browser and go to:`)); console.log(chalk.bold.white(verificationUrl)); } console.log(chalk.yellow(`\nEnter code: ${chalk.bold.white(deviceCode)}`)); console.log(chalk.gray('\nThe script will check for authentication every 5 seconds...')); // Wait for authentication return await this.waitForAuthentication(); } /** * Wait for user to complete authentication */ async waitForAuthentication() { const maxAttempts = 30; // 5 minutes total with exponential backoff let attempts = 0; let delay = 2000; // Start with 2 seconds console.log(chalk.blue('๐Ÿ”„ Waiting for authentication...')); while (attempts < maxAttempts) { attempts++; // Check auth status const statusResult = await this.callTool('check_github_auth'); const statusText = statusResult.content?.[0]?.text || ''; if (statusText.includes('โœ…') && statusText.includes('Authenticated')) { console.log(chalk.green('โœ… Authentication successful!')); console.log(chalk.green('\nโœ… GitHub OAuth authentication completed!')); return true; } // Check helper status for more details const helperStatus = await this.callTool('oauth_helper_status'); const helperText = helperStatus.content?.[0]?.text || ''; if (helperText.includes('โŒ') || helperText.includes('FAILED')) { console.log(chalk.red('โŒ Authentication failed')); console.log(chalk.red('\nโŒ Authentication failed or was cancelled')); return false; } // Calculate exponential backoff with cap at 10 seconds delay = Math.min(delay * 1.2, 10000); const totalElapsed = attempts * (delay / 1000); const remaining = Math.max(0, Math.floor((300 - totalElapsed) / 60)); process.stdout.write(`\r๐Ÿ”„ Waiting for authentication... (${remaining}m remaining, checking every ${Math.round(delay/1000)}s)`); // Wait with exponential backoff await new Promise(resolve => setTimeout(resolve, delay)); } console.log(chalk.red('\nโŒ Authentication timeout')); console.log(chalk.red('\nโฑ๏ธ Authentication timed out after 5 minutes')); return false; } /** * Test authenticated GitHub API access */ async testGitHubAccess() { console.log(chalk.yellow('\n๐Ÿงช Testing Authenticated GitHub API Access...\n')); // Test 1: Check authentication status console.log(chalk.blue('๐Ÿ”„ Verifying authentication...')); const authCheck = await this.callTool('check_github_auth'); console.log(chalk.green('โœ… Authentication verified')); // Test 2: Get user information (this would use OAuth if properly integrated) console.log(chalk.blue('\n๐Ÿ“Š Testing GitHub API Access through MCP...\n')); // Try to access a repository through MCP tools // Note: This assumes there are MCP tools that use GitHub API // You may need to add specific tools to your MCP server for this try { // Example: Try to list repositories or access portfolio const tools = await this.client.listTools(); // Look for GitHub-related tools const githubTools = tools.tools.filter(tool => tool.name.includes('github') || tool.name.includes('repository') || tool.name.includes('portfolio') ); if (githubTools.length > 0) { console.log(chalk.green(`Found ${githubTools.length} GitHub-related tools`)); // Test tools with masked names for debugging purposes let successCount = 0; let failCount = 0; console.log(chalk.blue('Testing GitHub tools...')); for (const tool of githubTools) { try { await this.callTool(tool.name); successCount++; } catch (error) { failCount++; } } console.log(chalk.green(`\n Summary: ${successCount} tools executed successfully`)); if (failCount > 0) { console.log(chalk.yellow(` ${failCount} tools failed`)); } } // Specific test: Try to access mickdarling/dollhouse-portfolio console.log(chalk.blue('\n๐ŸŽฏ Attempting to access mickdarling/dollhouse-portfolio...\n')); // This would need a specific MCP tool that uses GitHub API // For now, we'll check if the OAuth token is stored properly const helperStatus = await this.callTool('oauth_helper_status'); console.log(chalk.green('OAuth Helper Status:')); console.log(helperStatus.content?.[0]?.text || 'No status available'); } catch (error) { console.error(chalk.red('Error testing GitHub access:', error.message)); } } /** * Test accessing specific repository data */ async testRepositoryAccess(owner, repo, path) { console.log(chalk.blue(`\n๐Ÿ“ Testing access to ${owner}/${repo}/${path}...\n`)); // This would require specific MCP tools that use the OAuth token // to make authenticated GitHub API calls // For demonstration, we'll show what tools would be needed console.log(chalk.yellow('To fully test OAuth repository access, the MCP server needs tools that:')); console.log(' 1. Read the stored OAuth token'); console.log(' 2. Make authenticated GitHub API calls'); console.log(' 3. Return repository content'); console.log(chalk.cyan('\nExample MCP tool needed:')); console.log(chalk.gray(` async function getGitHubContent(owner, repo, path) { const token = await getStoredOAuthToken(); const response = await fetch( \`https://api.github.com/repos/\${owner}/\${repo}/contents/\${path}\`, { headers: { 'Authorization': \`token \${token}\`, 'Accept': 'application/vnd.github.v3+json' } } ); return await response.json(); } `)); } /** * Clean up and disconnect */ async cleanup() { console.log(chalk.yellow('\n๐Ÿงน Cleaning up...')); if (this.client) { await this.client.close(); } if (this.transport) { await this.transport.close(); } // Note: transport.close() handles process cleanup // No need to kill serverProcess separately since we removed the redundant spawn console.log(chalk.green('โœ… Cleanup complete')); } /** * Run the full test flow */ async run() { console.log(chalk.bold.cyan('\n' + '='.repeat(60))); const mode = isTestMode() ? '๐Ÿงช TEST MODE (PAT)' : '๐Ÿ” PRODUCTION MODE (OAuth)'; console.log(chalk.bold.white(` MCP Server OAuth Full Flow Test - ${mode}`)); console.log(chalk.bold.cyan('='.repeat(60) + '\n')); try { // Step 1: Connect to MCP await this.connectToMCP(); // Step 2: Start OAuth flow const authSuccess = await this.startOAuthFlow(); if (!authSuccess) { console.log(chalk.red('\nโŒ OAuth authentication failed')); return; } // Step 3: Test authenticated access await this.testGitHubAccess(); // Step 4: Test specific repository access await this.testRepositoryAccess('mickdarling', 'dollhouse-portfolio', 'templates'); console.log(chalk.bold.green('\n' + '='.repeat(60))); console.log(chalk.bold.white(' โœ… OAuth Flow Test Complete!')); console.log(chalk.bold.green('='.repeat(60) + '\n')); // Show summary console.log(chalk.cyan('Summary:')); console.log(chalk.green(' โœ… MCP server connection successful')); if (isTestMode()) { console.log(chalk.green(' โœ… PAT authentication verified (TEST MODE)')); console.log(chalk.yellow(' โš ๏ธ OAuth device flow NOT tested')); } else { console.log(chalk.green(' โœ… OAuth authentication completed')); console.log(chalk.green(' โœ… OAuth functionality verified')); } console.log(chalk.yellow(' โš ๏ธ Full API access requires additional MCP tools')); } catch (error) { console.error(chalk.red('\nโŒ Test failed:'), error.message); console.error(error.stack); } finally { await this.cleanup(); } } } // Run the test if (import.meta.url === `file://${process.argv[1]}`) { const tester = new MCPOAuthTester(); tester.run().catch(error => { console.error(chalk.red('Fatal error:'), error); process.exit(1); }); } export default MCPOAuthTester;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DollhouseMCP/DollhouseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server