Skip to main content
Glama
test-oauth-mcp-connection.cjs11 kB
#!/usr/bin/env node /** * Comprehensive OAuth + MCP Connection Test * Tests the entire authentication and connection flow */ const axios = require('axios'); const https = require('https'); const fs = require('fs'); const path = require('path'); // Ignore self-signed certificates for local testing const httpsAgent = new https.Agent({ rejectUnauthorized: false }); const BASE_URL = 'https://127.0.0.1:8787'; const CONFIG_PATH = path.join(__dirname, '../../config.json'); // Colors for console output const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } async function loadConfig() { try { const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); return config; } catch (error) { log(`❌ Failed to load config: ${error.message}`, 'red'); process.exit(1); } } async function testHealthCheck() { log('\n1. Testing Health Check Endpoint...', 'cyan'); try { const response = await axios.get(`${BASE_URL}/health`, { httpsAgent }); if (response.status === 200 && response.data.status === 'healthy') { log('✅ Health check passed', 'green'); log(` Version: ${response.data.version}`, 'blue'); log(` OAuth enabled: ${response.data.oauth}`, 'blue'); return true; } } catch (error) { log(`❌ Health check failed: ${error.message}`, 'red'); return false; } } async function testOAuthEndpoints(config) { log('\n2. Testing OAuth Endpoints...', 'cyan'); const issuer = config.auth.issuer || BASE_URL; const endpoints = [ { name: 'Authorization', url: `${issuer}/authorize`, method: 'GET' }, { name: 'Token', url: `${issuer}/oauth/token`, method: 'POST' }, { name: 'JWKS', url: `${issuer}/.well-known/jwks.json`, method: 'GET' } ]; let allPassed = true; for (const endpoint of endpoints) { try { const response = await axios({ method: endpoint.method, url: endpoint.url, httpsAgent, validateStatus: () => true // Accept any status }); if (endpoint.name === 'JWKS' && response.status === 200) { log(`✅ ${endpoint.name} endpoint accessible`, 'green'); log(` Keys available: ${response.data.keys?.length || 0}`, 'blue'); } else if (response.status < 500) { log(`✅ ${endpoint.name} endpoint accessible (${response.status})`, 'green'); } else { log(`⚠️ ${endpoint.name} endpoint returned ${response.status}`, 'yellow'); allPassed = false; } } catch (error) { log(`❌ ${endpoint.name} endpoint failed: ${error.message}`, 'red'); allPassed = false; } } return allPassed; } async function testMCPEndpoint() { log('\n3. Testing MCP Endpoint (Unauthenticated)...', 'cyan'); try { // Test HEAD request (capability check) const headResponse = await axios.head(`${BASE_URL}/mcp`, { httpsAgent, validateStatus: () => true }); if (headResponse.status === 401) { log('✅ HEAD /mcp returns 401 (expected)', 'green'); const wwwAuth = headResponse.headers['www-authenticate']; if (wwwAuth && wwwAuth.includes('Bearer')) { log('✅ WWW-Authenticate header present', 'green'); log(` ${wwwAuth.substring(0, 100)}...`, 'blue'); } } else { log(`⚠️ HEAD /mcp returned ${headResponse.status} (expected 401)`, 'yellow'); } // Test GET request (SSE) const getResponse = await axios.get(`${BASE_URL}/mcp`, { httpsAgent, validateStatus: () => true, headers: { 'Accept': 'text/event-stream' } }); if (getResponse.status === 401) { log('✅ GET /mcp returns 401 (expected)', 'green'); } else { log(`⚠️ GET /mcp returned ${getResponse.status} (expected 401)`, 'yellow'); } // Test POST request (JSON-RPC) const postResponse = await axios.post(`${BASE_URL}/mcp`, { jsonrpc: '2.0', method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {} }, id: 1 }, { httpsAgent, validateStatus: () => true } ); if (postResponse.status === 401) { log('✅ POST /mcp returns 401 (expected)', 'green'); if (postResponse.data.error?.data?.authorization_url) { log('✅ Authorization URL provided in error response', 'green'); log(` ${postResponse.data.error.data.authorization_url}`, 'blue'); } } else { log(`⚠️ POST /mcp returned ${postResponse.status} (expected 401)`, 'yellow'); } return true; } catch (error) { log(`❌ MCP endpoint test failed: ${error.message}`, 'red'); return false; } } async function testSSEWithMockToken() { log('\n4. Testing SSE with Mock Bearer Token...', 'cyan'); try { // Create a mock token (it will be invalid but tests the flow) const mockToken = 'mock.jwt.token'; const response = await axios.get(`${BASE_URL}/mcp`, { httpsAgent, validateStatus: () => true, headers: { 'Authorization': `Bearer ${mockToken}`, 'Accept': 'text/event-stream' }, responseType: 'stream', timeout: 2000 }); if (response.status === 401) { log('✅ SSE with invalid token returns 401 (expected)', 'green'); } else if (response.status === 200) { log('⚠️ SSE returned 200 with mock token (unexpected)', 'yellow'); // Try to read the stream briefly return new Promise((resolve) => { let data = ''; response.data.on('data', chunk => { data += chunk.toString(); }); setTimeout(() => { if (data.includes(':ping')) { log('✅ SSE sends ping and closes (minimal mechanism working)', 'green'); } else { log(` Received: ${data.substring(0, 100)}`, 'blue'); } resolve(true); }, 1000); }); } return true; } catch (error) { if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') { log('✅ SSE connection closed/timed out (expected with mock token)', 'green'); return true; } log(`❌ SSE test failed: ${error.message}`, 'red'); return false; } } async function checkServerStatus() { log('\n5. Checking Server Status...', 'cyan'); try { // Check if server is running const response = await axios.get(`${BASE_URL}/health`, { httpsAgent, timeout: 2000 }); if (response.status === 200) { log('✅ Server is running and responsive', 'green'); // Check for Cloudflare tunnel const config = await loadConfig(); if (config.cloudflare?.currentTunnelUrl) { log('✅ Cloudflare tunnel configured', 'green'); log(` Tunnel URL: ${config.cloudflare.currentTunnelUrl}`, 'blue'); // Test tunnel accessibility try { const tunnelResponse = await axios.get(`${config.cloudflare.currentTunnelUrl}/health`, { timeout: 5000 }); if (tunnelResponse.status === 200) { log('✅ Cloudflare tunnel is accessible', 'green'); } } catch (error) { log('⚠️ Cloudflare tunnel not accessible (may need to start tunnel)', 'yellow'); } } else { log('ℹ️ No Cloudflare tunnel configured (using local HTTPS)', 'blue'); } return true; } } catch (error) { log(`❌ Server is not responding: ${error.message}`, 'red'); log(' Please ensure the server is running with: npm run start:https', 'yellow'); return false; } } async function printConnectionInstructions(config) { log('\n' + '='.repeat(60), 'cyan'); log('CONNECTION INSTRUCTIONS', 'cyan'); log('='.repeat(60), 'cyan'); const issuer = config.auth.issuer || BASE_URL; const mcpUrl = config.cloudflare?.currentTunnelUrl ? `${config.cloudflare.currentTunnelUrl}/mcp` : `${BASE_URL}/mcp`; log('\n📝 To connect Claude Desktop to this MCP server:', 'green'); log('\n1. Add to Claude Desktop config (~/.config/claude/claude_desktop_config.json):', 'yellow'); log(JSON.stringify({ mcpServers: { "umbrella-mcp": { url: mcpUrl, transport: "http", headers: { "Authorization": "Bearer YOUR_TOKEN_HERE" } } } }, null, 2), 'blue'); log('\n2. OAuth Authentication Flow:', 'yellow'); log(` a. Open authorization URL: ${issuer}/authorize`, 'blue'); log(` b. Enter your Umbrella credentials:`, 'blue'); log(` - Email: your-email@company.com`, 'blue'); log(` - API Key: your-umbrella-api-key`, 'blue'); log(` - Bearer Token: your-umbrella-bearer-token`, 'blue'); log(` c. Copy the access token from the success page`, 'blue'); log(` d. Update the Authorization header in Claude config with:`, 'blue'); log(` "Authorization": "Bearer <YOUR_ACCESS_TOKEN>"`, 'blue'); log('\n3. Restart Claude Desktop', 'yellow'); log('\n4. Verify connection in Claude:', 'yellow'); log(' - Check the MCP icon in the bottom left', 'blue'); log(' - It should show "umbrella-mcp" as connected', 'blue'); log(' - Test with: "What cloud costs can you help me with?"', 'blue'); if (!config.cloudflare?.currentTunnelUrl) { log('\n⚠️ Note: You\'re using local HTTPS. For production:', 'yellow'); log(' - Run: npm run tunnel:start', 'blue'); log(' - This will create a public Cloudflare tunnel', 'blue'); } log('\n' + '='.repeat(60), 'cyan'); } async function runTests() { log('='.repeat(60), 'cyan'); log('MCP OAuth Connection Test Suite', 'cyan'); log('='.repeat(60), 'cyan'); const config = await loadConfig(); // Check server first if (!await checkServerStatus()) { log('\n❌ Server is not running. Start it with:', 'red'); log(' npm run build && npm run start:https', 'yellow'); process.exit(1); } // Run tests const tests = [ await testHealthCheck(), await testOAuthEndpoints(config), await testMCPEndpoint(), await testSSEWithMockToken() ]; const allPassed = tests.every(t => t); if (allPassed) { log('\n✅ All tests passed!', 'green'); await printConnectionInstructions(config); } else { log('\n⚠️ Some tests failed. Check the output above.', 'yellow'); log('Common issues:', 'yellow'); log(' - Server not running: npm run start:https', 'blue'); log(' - SSL certificates missing: npm run setup:certs', 'blue'); log(' - Config issues: Check config.json', 'blue'); } } // Run the tests runTests().catch(error => { log(`\n❌ Test suite failed: ${error.message}`, 'red'); process.exit(1); });

Latest Blog Posts

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/daviddraiumbrella/invoice-monitoring'

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