#!/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);
});