const { createHash, randomBytes } = require('crypto');
const express = require('express');
const { spawn } = require('child_process');
const https = require('https');
const fetch = require('node-fetch');
// Configure to ignore self-signed certificates for localhost
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
const LOCAL_MCP_URL = 'https://localhost:3003';
function base64URLEncode(str) {
return str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
function generateCodeChallenge() {
const codeVerifier = base64URLEncode(randomBytes(32));
const codeChallenge = base64URLEncode(createHash('sha256').update(codeVerifier).digest());
return { codeVerifier, codeChallenge };
}
class BankLeumiOAuthTest {
constructor() {
this.username = 'david+allcloud@umbrellacost.com';
this.password = 'Dsamsung1!';
this.accessToken = null;
this.server = null;
this.port = 3004; // Port that's allowed in TestUmbrellaMCP config
this.codeVerifier = null;
}
async startCallbackServer() {
return new Promise((resolve) => {
const app = express();
app.get('/', (req, res) => {
res.send(`
<html>
<head><title>Bank Leumi OAuth Test</title></head>
<body style="font-family: Arial; text-align: center; padding: 50px;">
<h1>🏦 Bank Leumi OAuth Test</h1>
<p><strong>Testing with userQuery parameter</strong></p>
</body>
</html>
`);
});
app.get('/callback', (req, res) => {
const { code, error, state } = req.query;
if (error) {
res.send(`<html><body>Error: ${error}</body></html>`);
return;
}
if (code) {
res.send(`
<html>
<body style="font-family: Arial; text-align: center; padding: 50px;">
<h1 style="color: green;">✅ OAuth Success!</h1>
<p>Testing Bank Leumi recommendations...</p>
<script>setTimeout(() => window.close(), 2000);</script>
</body>
</html>
`);
resolve(code);
}
});
this.server = app.listen(this.port, () => {
console.log(`📡 Callback server listening on port ${this.port}`);
});
});
}
async registerOAuthClient() {
// Use localhost on port 3004 as allowed in config
const redirectUri = `http://localhost:${this.port}/callback`;
const clientData = {
client_name: 'Bank Leumi OAuth Test Client',
redirect_uris: [redirectUri],
grant_types: ['authorization_code'],
response_types: ['code'],
scope: 'openid profile email'
};
const response = await fetch(`${LOCAL_MCP_URL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(clientData),
agent: new https.Agent({
rejectUnauthorized: false
})
});
if (!response.ok) {
throw new Error(`Client registration failed: ${response.status}`);
}
const client = await response.json();
console.log(`✅ Client registered: ${client.client_id}`);
return { clientId: client.client_id, redirectUri };
}
async performOAuthFlow(clientId, redirectUri) {
const { codeVerifier, codeChallenge } = generateCodeChallenge();
this.codeVerifier = codeVerifier;
const authUrl = `${LOCAL_MCP_URL}/authorize?` + new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
state: 'bank_leumi_oauth_test',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
scope: 'openid profile email'
});
console.log('🌐 Opening browser for authentication...');
this.openBrowser(authUrl);
return true;
}
openBrowser(url) {
const platform = process.platform;
let command;
if (platform === 'darwin') {
command = 'open';
} else if (platform === 'win32') {
command = 'start';
} else {
command = 'xdg-open';
}
spawn(command, [url], { detached: true, stdio: 'ignore' });
}
async exchangeCodeForToken(authorizationCode, clientId, redirectUri) {
const response = await fetch(`${LOCAL_MCP_URL}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: redirectUri,
client_id: clientId,
code_verifier: this.codeVerifier
}),
agent: new https.Agent({
rejectUnauthorized: false
})
});
if (response.ok) {
const tokenData = await response.json();
this.accessToken = tokenData.access_token;
console.log(`✅ Token obtained successfully`);
return tokenData;
} else {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
}
async testBankLeumiRecommendations() {
if (!this.accessToken) {
console.log('❌ No bearer token available');
return false;
}
console.log(`\n🚀 Testing Bank Leumi recommendations with userQuery`);
console.log(`🔑 Bearer Token: ${this.accessToken.substring(0, 50)}...`);
try {
// Test 1: Bank Leumi BL Test Env with userQuery
console.log('\n📊 Test 1: Bank Leumi BL Test Env - with userQuery');
console.log('Expected: Should use account key 24223');
console.log('Expected API key format: 57ade50e-c9a8-49f3-8ce7-28d44536a669:24223:1');
const request1 = {
jsonrpc: '2.0',
method: 'tools/call',
params: {
name: 'get_all_recommendations',
arguments: {
userQuery: 'show me recommendations for Bank Leumi BL Test Env',
daysBack: 30,
pageSize: 100,
includeClosedAndDone: false
}
},
id: 1
};
console.log('📤 Request:', JSON.stringify(request1.params.arguments, null, 2));
const startTime1 = Date.now();
const response1 = await fetch(`${LOCAL_MCP_URL}/mcp`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify(request1),
agent: new https.Agent({
rejectUnauthorized: false
})
});
const time1 = Date.now() - startTime1;
if (response1.ok) {
let data1;
const contentType = response1.headers.get('content-type');
if (contentType && contentType.includes('text/event-stream')) {
const responseText = await response1.text();
const lines = responseText.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.substring(6).trim();
if (jsonStr && jsonStr !== '[DONE]') {
try {
data1 = JSON.parse(jsonStr);
break;
} catch (parseError) {}
}
}
}
} else {
data1 = await response1.json();
}
console.log(`✅ Test 1 SUCCESS (${time1}ms)`);
if (data1 && data1.result && data1.result.content && data1.result.content[0]) {
const text = data1.result.content[0].text;
const customerMatch = text.match(/\*\*Customer:\*\* ([^\n]+)/);
const totalMatch = text.match(/\*\*Total Recommendations:\*\* (\d+)/);
const savingsMatch = text.match(/\*\*Total Potential Savings:\*\* \$[\d,]+\.\d{2}/);
if (customerMatch) {
console.log(` Detected Customer: ${customerMatch[1]}`);
}
if (totalMatch) {
console.log(` Total Recommendations: ${totalMatch[1]}`);
}
if (savingsMatch) {
console.log(` ${savingsMatch[0].replace(/\*\*/g, '')}`);
}
// Show first few recommendations
const recsMatch = text.match(/## Top \d+ Recommendations by Savings:[\s\S]*?(?=##|$)/);
if (recsMatch) {
console.log('\n First few recommendations:');
const recs = recsMatch[0].split(/\d+\.\s+\*\*/).slice(1, 4);
recs.forEach((rec, i) => {
const lines = rec.split('\n').filter(l => l.trim());
if (lines[0]) {
console.log(` ${i + 1}. ${lines[0].replace(/\*\*/g, '').trim()}`);
}
});
}
}
} else {
console.log(`❌ Test 1 FAILED: ${response1.status}`);
const error = await response1.text();
console.log('Error:', error);
}
// Test 2: Without userQuery (should default to first account)
console.log('\n📊 Test 2: Without userQuery - should use default account');
const request2 = {
jsonrpc: '2.0',
method: 'tools/call',
params: {
name: 'get_all_recommendations',
arguments: {
daysBack: 7,
pageSize: 50
}
},
id: 2
};
console.log('📤 Request:', JSON.stringify(request2.params.arguments, null, 2));
const startTime2 = Date.now();
const response2 = await fetch(`${LOCAL_MCP_URL}/mcp`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify(request2),
agent: new https.Agent({
rejectUnauthorized: false
})
});
const time2 = Date.now() - startTime2;
if (response2.ok) {
let data2;
const contentType = response2.headers.get('content-type');
if (contentType && contentType.includes('text/event-stream')) {
const responseText = await response2.text();
const lines = responseText.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.substring(6).trim();
if (jsonStr && jsonStr !== '[DONE]') {
try {
data2 = JSON.parse(jsonStr);
break;
} catch (parseError) {}
}
}
}
} else {
data2 = await response2.json();
}
console.log(`✅ Test 2 SUCCESS (${time2}ms)`);
if (data2 && data2.result && data2.result.content && data2.result.content[0]) {
const text = data2.result.content[0].text;
const customerMatch = text.match(/\*\*Customer:\*\* ([^\n]+)/);
if (customerMatch) {
console.log(` Default Customer: ${customerMatch[1]}`);
}
}
} else {
console.log(`❌ Test 2 FAILED: ${response2.status}`);
}
console.log('\n✅ Bank Leumi OAuth Tests Complete!');
console.log('\n📝 Check server logs for:');
console.log(' - Customer detection from userQuery');
console.log(' - API key generation with correct format');
return true;
} catch (error) {
console.log('❌ Test Error:', error.message);
return false;
}
}
cleanup() {
if (this.server) {
this.server.close();
}
}
async run() {
try {
console.log('🏦 Bank Leumi OAuth Test Starting...\n');
const serverPromise = this.startCallbackServer();
const { clientId, redirectUri } = await this.registerOAuthClient();
const flowStarted = await this.performOAuthFlow(clientId, redirectUri);
if (!flowStarted) {
throw new Error('Failed to start OAuth flow');
}
console.log('⏳ Waiting for authentication...');
console.log(' Please login with: david+allcloud@umbrellacost.com');
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('OAuth timeout after 5 minutes')), 300000)
);
const authorizationCode = await Promise.race([serverPromise, timeoutPromise]);
if (authorizationCode) {
const tokenData = await this.exchangeCodeForToken(authorizationCode, clientId, redirectUri);
if (tokenData) {
await this.testBankLeumiRecommendations();
}
}
} catch (error) {
console.error('❌ Error:', error.message);
} finally {
this.cleanup();
}
}
}
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Interrupted - cleaning up...');
process.exit(0);
});
const test = new BankLeumiOAuthTest();
test.run();