ft-scout 9.2.0 → 9.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.firebase/hosting.d2Vi.cache +9 -9
- package/.mcp-config.json +19 -0
- package/.mft-mcp-config.json +29 -0
- package/README.md +2 -2
- package/bin/src/commands/authCommand.d.ts.map +1 -1
- package/bin/src/commands/authCommand.js +13 -2
- package/bin/src/commands/authCommand.js.map +1 -1
- package/bin/src/commands/login.d.ts.map +1 -1
- package/bin/src/commands/login.js +39 -6
- package/bin/src/commands/login.js.map +1 -1
- package/bin/src/commands/logout.d.ts.map +1 -1
- package/bin/src/commands/logout.js +67 -2
- package/bin/src/commands/logout.js.map +1 -1
- package/bin/src/commands/mcp.d.ts.map +1 -1
- package/bin/src/commands/mcp.js +6 -1
- package/bin/src/commands/mcp.js.map +1 -1
- package/bin/src/commands/switchCommand.d.ts.map +1 -0
- package/bin/src/commands/switchCommand.js +178 -0
- package/bin/src/commands/switchCommand.js.map +1 -0
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +24 -12
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/toolRegistry.d.ts.map +1 -1
- package/bin/src/engine/toolRegistry.js +1 -0
- package/bin/src/engine/toolRegistry.js.map +1 -1
- package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -1
- package/bin/src/engine/workspace/agentWorkspace.js +6 -4
- package/bin/src/engine/workspace/agentWorkspace.js.map +1 -1
- package/bin/src/index.js +28 -9
- package/bin/src/index.js.map +1 -1
- package/bin/src/mcp/client.js +1 -1
- package/bin/src/mcp/cloudSync.d.ts.map +1 -0
- package/bin/src/mcp/cloudSync.js +109 -0
- package/bin/src/mcp/cloudSync.js.map +1 -0
- package/bin/src/mcp/index.d.ts.map +1 -1
- package/bin/src/mcp/index.js +1 -0
- package/bin/src/mcp/index.js.map +1 -1
- package/bin/src/mcp/manager.d.ts.map +1 -1
- package/bin/src/mcp/manager.js +40 -1
- package/bin/src/mcp/manager.js.map +1 -1
- package/bin/src/server/server.d.ts.map +1 -1
- package/bin/src/server/server.js +89 -9
- package/bin/src/server/server.js.map +1 -1
- package/bin/src/utils/auth.d.ts.map +1 -1
- package/bin/src/utils/auth.js +211 -0
- package/bin/src/utils/auth.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/bin/src/utils/qrBridge.d.ts.map +1 -0
- package/bin/src/utils/qrBridge.js +216 -0
- package/bin/src/utils/qrBridge.js.map +1 -0
- package/package.json +2 -2
- package/web/app.js +497 -52
- package/web/styles.css +44 -2
package/web/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
FrontTerrain Scout Multi-Page Application Engine v9.
|
|
2
|
+
FrontTerrain Scout Multi-Page Application Engine v9.5.0
|
|
3
3
|
========================================================================== */
|
|
4
4
|
|
|
5
5
|
const FIREBASE_CONFIG = {
|
|
@@ -1103,7 +1103,7 @@ function initTerminalSimulator() {
|
|
|
1103
1103
|
`;
|
|
1104
1104
|
} else if (lower.includes('status')) {
|
|
1105
1105
|
response.innerHTML = `
|
|
1106
|
-
<span class="term-green">✔ Scout CLI Engine: v9.
|
|
1106
|
+
<span class="term-green">✔ Scout CLI Engine: v9.5.0 Online</span><br>
|
|
1107
1107
|
↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
|
|
1108
1108
|
↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
|
|
1109
1109
|
↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
|
|
@@ -1881,19 +1881,187 @@ function updateUserUI() {
|
|
|
1881
1881
|
}
|
|
1882
1882
|
}
|
|
1883
1883
|
|
|
1884
|
+
// Render multi-account switching list in dropdown
|
|
1885
|
+
renderDropdownAccounts();
|
|
1886
|
+
|
|
1884
1887
|
// Refresh MCP servers scoped to active user
|
|
1885
1888
|
if (typeof loadMcpData === 'function') {
|
|
1886
1889
|
loadMcpData();
|
|
1887
1890
|
}
|
|
1888
1891
|
}
|
|
1889
1892
|
|
|
1893
|
+
function getSavedAccountsFromStorage() {
|
|
1894
|
+
try {
|
|
1895
|
+
const raw = localStorage.getItem('scout_accounts');
|
|
1896
|
+
return raw ? JSON.parse(raw) : [];
|
|
1897
|
+
} catch {
|
|
1898
|
+
return [];
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function saveAccountToStorageList(accountState) {
|
|
1903
|
+
if (!accountState?.user?.email) return;
|
|
1904
|
+
const accounts = getSavedAccountsFromStorage();
|
|
1905
|
+
const key = accountState.user.email.toLowerCase();
|
|
1906
|
+
const idx = accounts.findIndex(a => a.user?.email?.toLowerCase() === key);
|
|
1907
|
+
const item = {
|
|
1908
|
+
user: accountState.user,
|
|
1909
|
+
token: accountState.token,
|
|
1910
|
+
refreshToken: accountState.refreshToken,
|
|
1911
|
+
currentTier: accountState.currentTier || 'free',
|
|
1912
|
+
creditsUsed: accountState.creditsUsed ?? 0,
|
|
1913
|
+
creditsTotal: accountState.creditsTotal ?? 100,
|
|
1914
|
+
renewAt: accountState.renewAt,
|
|
1915
|
+
paidTier: accountState.paidTier,
|
|
1916
|
+
lastActiveAt: new Date().toISOString()
|
|
1917
|
+
};
|
|
1918
|
+
if (idx >= 0) {
|
|
1919
|
+
accounts[idx] = item;
|
|
1920
|
+
} else {
|
|
1921
|
+
accounts.push(item);
|
|
1922
|
+
}
|
|
1923
|
+
localStorage.setItem('scout_accounts', JSON.stringify(accounts));
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
async function renderDropdownAccounts() {
|
|
1927
|
+
const dropdown = getEl('user-dropdown');
|
|
1928
|
+
if (!dropdown) return;
|
|
1929
|
+
|
|
1930
|
+
let container = getEl('dropdown-accounts-container');
|
|
1931
|
+
if (!container) {
|
|
1932
|
+
container = document.createElement('div');
|
|
1933
|
+
container.id = 'dropdown-accounts-container';
|
|
1934
|
+
const logoutBtn = getEl('btn-dropdown-logout');
|
|
1935
|
+
if (logoutBtn) {
|
|
1936
|
+
dropdown.insertBefore(container, logoutBtn);
|
|
1937
|
+
} else {
|
|
1938
|
+
dropdown.appendChild(container);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
let accounts = getSavedAccountsFromStorage();
|
|
1943
|
+
if (state.serverConnected) {
|
|
1944
|
+
try {
|
|
1945
|
+
const res = await fetch(`${state.apiBaseUrl}/api/accounts`);
|
|
1946
|
+
const data = await res.json();
|
|
1947
|
+
if (data.success && Array.isArray(data.accounts)) {
|
|
1948
|
+
accounts = data.accounts;
|
|
1949
|
+
localStorage.setItem('scout_accounts', JSON.stringify(accounts));
|
|
1950
|
+
}
|
|
1951
|
+
} catch { }
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
const currentEmail = (state.user?.email || '').toLowerCase();
|
|
1955
|
+
const otherAccounts = accounts.filter(a => (a.user?.email || '').toLowerCase() !== currentEmail);
|
|
1956
|
+
|
|
1957
|
+
let html = '<hr class="dropdown-divider">';
|
|
1958
|
+
html += '<div class="dropdown-header" style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.7; padding-bottom: 4px;">Saved Accounts</div>';
|
|
1959
|
+
|
|
1960
|
+
if (otherAccounts.length > 0) {
|
|
1961
|
+
otherAccounts.forEach(acc => {
|
|
1962
|
+
const email = acc.user?.email || 'Unknown';
|
|
1963
|
+
const tier = (acc.currentTier || 'free').toUpperCase();
|
|
1964
|
+
html += `
|
|
1965
|
+
<button type="button" class="dropdown-item btn-switch-account" data-email="${escapeHtml(email)}" style="display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; border: none; background: transparent; cursor: pointer; text-align: left;">
|
|
1966
|
+
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><i class="fa-solid fa-circle-user" style="margin-right: 6px;"></i> ${escapeHtml(email)}</span>
|
|
1967
|
+
<span class="badge" style="font-size: 9px; padding: 2px 6px; border-radius: 4px; background: rgba(255,255,255,0.1);">${escapeHtml(tier)}</span>
|
|
1968
|
+
</button>
|
|
1969
|
+
`;
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
html += `
|
|
1974
|
+
<a href="/auth?add_account=true" class="dropdown-item" id="btn-dropdown-add-account" style="color: var(--color-primary, #10b981);">
|
|
1975
|
+
<i class="fa-solid fa-user-plus"></i> Add Another Account
|
|
1976
|
+
</a>
|
|
1977
|
+
`;
|
|
1978
|
+
|
|
1979
|
+
container.innerHTML = html;
|
|
1980
|
+
|
|
1981
|
+
container.querySelectorAll('.btn-switch-account').forEach(btn => {
|
|
1982
|
+
btn.addEventListener('click', (e) => {
|
|
1983
|
+
e.stopPropagation();
|
|
1984
|
+
const targetEmail = btn.dataset.email;
|
|
1985
|
+
if (targetEmail) {
|
|
1986
|
+
switchAccountWeb(targetEmail);
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
async function switchAccountWeb(targetEmail) {
|
|
1993
|
+
showToast(`Switching active account to ${targetEmail}...`, 'info');
|
|
1994
|
+
if (state.serverConnected) {
|
|
1995
|
+
try {
|
|
1996
|
+
const res = await fetch(`${state.apiBaseUrl}/api/accounts/switch`, {
|
|
1997
|
+
method: 'POST',
|
|
1998
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1999
|
+
body: JSON.stringify({ email: targetEmail })
|
|
2000
|
+
});
|
|
2001
|
+
const data = await res.json();
|
|
2002
|
+
if (data.success && data.auth) {
|
|
2003
|
+
state.user = data.auth.user;
|
|
2004
|
+
state.token = data.auth.token;
|
|
2005
|
+
state.refreshToken = data.auth.refreshToken || null;
|
|
2006
|
+
state.currentTier = data.auth.currentTier || 'free';
|
|
2007
|
+
state.creditsUsed = data.auth.creditsUsed ?? 0;
|
|
2008
|
+
state.creditsTotal = data.auth.creditsTotal ?? 100;
|
|
2009
|
+
state.renewAt = data.auth.renewAt || null;
|
|
2010
|
+
state.byokEnabled = Boolean(data.auth.byokEnabled);
|
|
2011
|
+
state.customLlmProvider = data.auth.customLlmProvider || 'nvidia';
|
|
2012
|
+
state.customApiKey = data.auth.customApiKey || '';
|
|
2013
|
+
state.customBaseUrl = data.auth.customBaseUrl || '';
|
|
2014
|
+
|
|
2015
|
+
saveSessionToStorage();
|
|
2016
|
+
updateUserUI();
|
|
2017
|
+
showToast(`Switched active account to ${targetEmail}`, 'success');
|
|
2018
|
+
setTimeout(() => { window.location.reload(); }, 300);
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
} catch { }
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
const accounts = getSavedAccountsFromStorage();
|
|
2025
|
+
const target = accounts.find(a => (a.user?.email || '').toLowerCase() === targetEmail.toLowerCase());
|
|
2026
|
+
if (target) {
|
|
2027
|
+
state.user = target.user;
|
|
2028
|
+
state.token = target.token;
|
|
2029
|
+
state.refreshToken = target.refreshToken;
|
|
2030
|
+
state.currentTier = target.currentTier || 'free';
|
|
2031
|
+
state.creditsUsed = target.creditsUsed ?? 0;
|
|
2032
|
+
state.creditsTotal = target.creditsTotal ?? 100;
|
|
2033
|
+
state.renewAt = target.renewAt;
|
|
2034
|
+
|
|
2035
|
+
saveSessionToStorage();
|
|
2036
|
+
updateUserUI();
|
|
2037
|
+
showToast(`Switched active account to ${targetEmail}`, 'success');
|
|
2038
|
+
setTimeout(() => { window.location.reload(); }, 300);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1890
2042
|
async function handleLogout() {
|
|
2043
|
+
const currentEmail = (state.user?.email || '').toLowerCase();
|
|
2044
|
+
let accounts = getSavedAccountsFromStorage();
|
|
2045
|
+
accounts = accounts.filter(a => (a.user?.email || '').toLowerCase() !== currentEmail);
|
|
2046
|
+
localStorage.setItem('scout_accounts', JSON.stringify(accounts));
|
|
2047
|
+
|
|
1891
2048
|
if (state.serverConnected) {
|
|
1892
2049
|
try {
|
|
1893
|
-
await fetch(`${state.apiBaseUrl}/api/logout`, {
|
|
2050
|
+
await fetch(`${state.apiBaseUrl}/api/logout`, {
|
|
2051
|
+
method: 'POST',
|
|
2052
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2053
|
+
body: JSON.stringify({ email: currentEmail })
|
|
2054
|
+
});
|
|
1894
2055
|
} catch { }
|
|
1895
2056
|
}
|
|
1896
2057
|
|
|
2058
|
+
if (accounts.length > 0) {
|
|
2059
|
+
const next = accounts[0];
|
|
2060
|
+
switchAccountWeb(next.user.email);
|
|
2061
|
+
showToast(`Logged out. Switched to ${next.user.email}.`, 'info');
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1897
2065
|
state.user = null;
|
|
1898
2066
|
state.token = null;
|
|
1899
2067
|
state.refreshToken = null;
|
|
@@ -1929,6 +2097,7 @@ function saveSessionToStorage() {
|
|
|
1929
2097
|
localStorage.setItem('scout_byok_provider', state.customLlmProvider);
|
|
1930
2098
|
localStorage.setItem('scout_byok_key', state.customApiKey);
|
|
1931
2099
|
localStorage.setItem('scout_byok_endpoint', state.customBaseUrl);
|
|
2100
|
+
saveAccountToStorageList(state);
|
|
1932
2101
|
}
|
|
1933
2102
|
|
|
1934
2103
|
async function syncAuthWithServer() {
|
|
@@ -3352,6 +3521,153 @@ async function connectRemoteMcpInBrowser(server) {
|
|
|
3352
3521
|
return { tools, latencyMs };
|
|
3353
3522
|
}
|
|
3354
3523
|
|
|
3524
|
+
let mcpSyncPollerTimer = null;
|
|
3525
|
+
const FIRESTORE_MCP_BASE = `https://firestore.googleapis.com/v1/projects/${FIREBASE_CONFIG.projectId}/databases/(default)/documents/mcp_servers`;
|
|
3526
|
+
|
|
3527
|
+
async function fetchCloudMcpServers(filterEmail) {
|
|
3528
|
+
try {
|
|
3529
|
+
const controller = new AbortController();
|
|
3530
|
+
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
|
3531
|
+
const url = `${FIRESTORE_MCP_BASE}?key=${FIREBASE_CONFIG.apiKey}&pageSize=100`;
|
|
3532
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
3533
|
+
clearTimeout(timeoutId);
|
|
3534
|
+
if (!res.ok) return null;
|
|
3535
|
+
const data = await res.json();
|
|
3536
|
+
if (!data.documents || !Array.isArray(data.documents)) return [];
|
|
3537
|
+
const servers = [];
|
|
3538
|
+
const normEmail = filterEmail ? filterEmail.trim().toLowerCase() : '';
|
|
3539
|
+
for (const doc of data.documents) {
|
|
3540
|
+
try {
|
|
3541
|
+
const fields = doc.fields || {};
|
|
3542
|
+
const jsonStr = fields.dataJson?.stringValue;
|
|
3543
|
+
if (!jsonStr) continue;
|
|
3544
|
+
const server = JSON.parse(jsonStr);
|
|
3545
|
+
const docEmail = (fields.userEmail?.stringValue || server.userEmail || '').trim().toLowerCase();
|
|
3546
|
+
if (normEmail && docEmail && docEmail !== normEmail && docEmail !== 'global' && docEmail !== 'default') {
|
|
3547
|
+
continue;
|
|
3548
|
+
}
|
|
3549
|
+
servers.push(server);
|
|
3550
|
+
} catch {}
|
|
3551
|
+
}
|
|
3552
|
+
return servers;
|
|
3553
|
+
} catch (e) {
|
|
3554
|
+
console.warn('[MCP Cloud] Failed to fetch cloud MCP servers:', e);
|
|
3555
|
+
return null;
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
async function pushCloudMcpServer(server) {
|
|
3560
|
+
try {
|
|
3561
|
+
const controller = new AbortController();
|
|
3562
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
3563
|
+
const docId = encodeURIComponent(server.id);
|
|
3564
|
+
const url = `${FIRESTORE_MCP_BASE}/${docId}?key=${FIREBASE_CONFIG.apiKey}`;
|
|
3565
|
+
const now = new Date().toISOString();
|
|
3566
|
+
const payload = {
|
|
3567
|
+
fields: {
|
|
3568
|
+
id: { stringValue: server.id },
|
|
3569
|
+
name: { stringValue: server.name || '' },
|
|
3570
|
+
transport: { stringValue: server.transport || 'http' },
|
|
3571
|
+
endpoint: { stringValue: server.endpoint || '' },
|
|
3572
|
+
command: { stringValue: server.command || '' },
|
|
3573
|
+
userEmail: { stringValue: server.userEmail || '' },
|
|
3574
|
+
userId: { stringValue: server.userId || '' },
|
|
3575
|
+
status: { stringValue: server.status || 'connected' },
|
|
3576
|
+
toolCount: { integerValue: String(server.tools?.length || 0) },
|
|
3577
|
+
updatedAt: { stringValue: now },
|
|
3578
|
+
dataJson: { stringValue: JSON.stringify(server) }
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
const res = await fetch(url, {
|
|
3582
|
+
method: 'PATCH',
|
|
3583
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3584
|
+
body: JSON.stringify(payload),
|
|
3585
|
+
signal: controller.signal
|
|
3586
|
+
});
|
|
3587
|
+
clearTimeout(timeoutId);
|
|
3588
|
+
return res.ok;
|
|
3589
|
+
} catch (e) {
|
|
3590
|
+
console.warn('[MCP Cloud] Failed to push MCP server to cloud:', e);
|
|
3591
|
+
return false;
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
async function deleteCloudMcpServer(serverId) {
|
|
3596
|
+
try {
|
|
3597
|
+
const controller = new AbortController();
|
|
3598
|
+
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
|
3599
|
+
const docId = encodeURIComponent(serverId);
|
|
3600
|
+
const url = `${FIRESTORE_MCP_BASE}/${docId}?key=${FIREBASE_CONFIG.apiKey}`;
|
|
3601
|
+
const res = await fetch(url, {
|
|
3602
|
+
method: 'DELETE',
|
|
3603
|
+
signal: controller.signal
|
|
3604
|
+
});
|
|
3605
|
+
clearTimeout(timeoutId);
|
|
3606
|
+
return res.ok;
|
|
3607
|
+
} catch (e) {
|
|
3608
|
+
console.warn('[MCP Cloud] Failed to delete MCP server from cloud:', e);
|
|
3609
|
+
return false;
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
async function getMcpApiBase() {
|
|
3614
|
+
if (state.serverConnected && state.apiBaseUrl !== undefined) {
|
|
3615
|
+
return state.apiBaseUrl;
|
|
3616
|
+
}
|
|
3617
|
+
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.protocol === 'file:';
|
|
3618
|
+
if (isLocalhost) {
|
|
3619
|
+
try {
|
|
3620
|
+
const res = await fetch('/api/status', { method: 'GET' });
|
|
3621
|
+
if (res.ok) {
|
|
3622
|
+
const data = await res.json();
|
|
3623
|
+
if (data && data.status === 'ok') {
|
|
3624
|
+
state.apiBaseUrl = '';
|
|
3625
|
+
state.serverConnected = true;
|
|
3626
|
+
return '';
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
} catch { }
|
|
3630
|
+
}
|
|
3631
|
+
const candidateHosts = ['http://localhost:9120', 'http://127.0.0.1:9120'];
|
|
3632
|
+
for (const host of candidateHosts) {
|
|
3633
|
+
try {
|
|
3634
|
+
const controller = new AbortController();
|
|
3635
|
+
const timeoutId = setTimeout(() => controller.abort(), 800);
|
|
3636
|
+
const res = await fetch(`${host}/api/status`, { method: 'GET', signal: controller.signal });
|
|
3637
|
+
clearTimeout(timeoutId);
|
|
3638
|
+
if (res.ok) {
|
|
3639
|
+
const data = await res.json();
|
|
3640
|
+
if (data && data.status === 'ok') {
|
|
3641
|
+
state.apiBaseUrl = host;
|
|
3642
|
+
state.serverConnected = true;
|
|
3643
|
+
return host;
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
} catch { }
|
|
3647
|
+
}
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
function renderMcpSyncStatus() {
|
|
3652
|
+
const container = getEl('mcp-sync-status-container');
|
|
3653
|
+
if (!container) return;
|
|
3654
|
+
|
|
3655
|
+
container.innerHTML = `
|
|
3656
|
+
<div class="mcp-sync-banner connected">
|
|
3657
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
|
3658
|
+
<span class="blink-dot" style="background-color: #10b981; width: 9px; height: 9px; border-radius: 50%; display: inline-block;"></span>
|
|
3659
|
+
<span><strong>Cloud 2-Way CLI Sync Active:</strong> Synchronized with FrontTerrain Cloud Ecosystem. Changes instantly reflect in <code>scout mcp list</code>.</span>
|
|
3660
|
+
</div>
|
|
3661
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
3662
|
+
<span style="font-size: 11px; opacity: 0.85;"><i class="fa-solid fa-arrows-rotate fa-spin" style="margin-right: 4px; color: #10b981;"></i> Cloud Sync</span>
|
|
3663
|
+
<button class="btn btn-sm btn-outline" onclick="loadMcpData(true)" style="padding: 2px 8px; font-size: 11px;" title="Sync latest servers from Cloud">
|
|
3664
|
+
<i class="fa-solid fa-rotate-right"></i> Refresh
|
|
3665
|
+
</button>
|
|
3666
|
+
</div>
|
|
3667
|
+
</div>
|
|
3668
|
+
`;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3355
3671
|
const mcpState = {
|
|
3356
3672
|
servers: (() => {
|
|
3357
3673
|
try {
|
|
@@ -3368,10 +3684,11 @@ const mcpState = {
|
|
|
3368
3684
|
loading: false
|
|
3369
3685
|
};
|
|
3370
3686
|
|
|
3371
|
-
async function loadMcpData() {
|
|
3687
|
+
async function loadMcpData(forceProbe = false) {
|
|
3372
3688
|
const userEmail = (state.user?.email || '').trim().toLowerCase();
|
|
3373
3689
|
const storageKey = getAccountMcpStorageKey();
|
|
3374
3690
|
|
|
3691
|
+
// 1. Instantly load local cache so UI is responsive
|
|
3375
3692
|
try {
|
|
3376
3693
|
const cached = localStorage.getItem(storageKey) || localStorage.getItem('scout_cached_mcp_servers');
|
|
3377
3694
|
if (cached) {
|
|
@@ -3395,8 +3712,81 @@ async function loadMcpData() {
|
|
|
3395
3712
|
renderMcpKpis();
|
|
3396
3713
|
renderMcpServersList();
|
|
3397
3714
|
renderMcpActivityList();
|
|
3715
|
+
renderMcpSyncStatus();
|
|
3716
|
+
|
|
3717
|
+
// 2. Fetch directly from Cloud Firestore (primary 2-way sync with CLI)
|
|
3718
|
+
try {
|
|
3719
|
+
const cloudServers = await fetchCloudMcpServers(userEmail);
|
|
3720
|
+
if (cloudServers !== null && Array.isArray(cloudServers)) {
|
|
3721
|
+
const serverMap = new Map();
|
|
3722
|
+
for (const s of mcpState.servers) {
|
|
3723
|
+
if (s && s.id) serverMap.set(s.id, s);
|
|
3724
|
+
}
|
|
3725
|
+
for (const s of cloudServers) {
|
|
3726
|
+
if (s && s.id) serverMap.set(s.id, s);
|
|
3727
|
+
}
|
|
3728
|
+
mcpState.servers = Array.from(serverMap.values());
|
|
3729
|
+
try {
|
|
3730
|
+
localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
|
|
3731
|
+
localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
|
|
3732
|
+
} catch { }
|
|
3733
|
+
renderMcpKpis();
|
|
3734
|
+
renderMcpServersList();
|
|
3735
|
+
}
|
|
3736
|
+
} catch (e) {
|
|
3737
|
+
console.warn('Cloud MCP sync fetch error:', e);
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
// 3. Optional local bridge sync if local server happens to be running
|
|
3741
|
+
const apiBase = await getMcpApiBase();
|
|
3742
|
+
if (apiBase !== null) {
|
|
3743
|
+
try {
|
|
3744
|
+
const actRes = await fetch(`${apiBase}/api/mcp/activity?limit=50`, { method: 'GET' });
|
|
3745
|
+
if (actRes.ok) {
|
|
3746
|
+
const actData = await actRes.json();
|
|
3747
|
+
if (actData.success && Array.isArray(actData.activity) && actData.activity.length > 0) {
|
|
3748
|
+
mcpState.activity = actData.activity;
|
|
3749
|
+
renderMcpActivityList();
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
} catch { }
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
// 4. Setup background live poller (every 4s on #connections view)
|
|
3756
|
+
if (!mcpSyncPollerTimer) {
|
|
3757
|
+
mcpSyncPollerTimer = setInterval(async () => {
|
|
3758
|
+
const isConnView = window.location.hash === '#connections' || document.getElementById('view-connections')?.classList.contains('active');
|
|
3759
|
+
if (!isConnView) return;
|
|
3760
|
+
|
|
3761
|
+
try {
|
|
3762
|
+
const cloudServers = await fetchCloudMcpServers(userEmail);
|
|
3763
|
+
if (cloudServers !== null && Array.isArray(cloudServers)) {
|
|
3764
|
+
const serverMap = new Map();
|
|
3765
|
+
for (const s of mcpState.servers) {
|
|
3766
|
+
if (s && s.id) serverMap.set(s.id, s);
|
|
3767
|
+
}
|
|
3768
|
+
for (const s of cloudServers) {
|
|
3769
|
+
if (s && s.id) serverMap.set(s.id, s);
|
|
3770
|
+
}
|
|
3771
|
+
const merged = Array.from(serverMap.values());
|
|
3772
|
+
const newJson = JSON.stringify(merged);
|
|
3773
|
+
const oldJson = JSON.stringify(mcpState.servers);
|
|
3774
|
+
if (newJson !== oldJson) {
|
|
3775
|
+
mcpState.servers = merged;
|
|
3776
|
+
try {
|
|
3777
|
+
localStorage.setItem(getAccountMcpStorageKey(), newJson);
|
|
3778
|
+
localStorage.setItem('scout_cached_mcp_servers', newJson);
|
|
3779
|
+
} catch { }
|
|
3780
|
+
renderMcpKpis();
|
|
3781
|
+
renderMcpServersList();
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
} catch { }
|
|
3785
|
+
}, 4000);
|
|
3786
|
+
}
|
|
3398
3787
|
}
|
|
3399
3788
|
|
|
3789
|
+
|
|
3400
3790
|
function renderMcpKpis() {
|
|
3401
3791
|
const serversEl = getEl('mcp-stat-servers');
|
|
3402
3792
|
const toolsEl = getEl('mcp-stat-tools');
|
|
@@ -3677,24 +4067,7 @@ async function handleAddMcpServerSubmit(e) {
|
|
|
3677
4067
|
try {
|
|
3678
4068
|
const userEmail = (state.user?.email || '').trim().toLowerCase();
|
|
3679
4069
|
const userId = state.user?.id || state.user?.uid || undefined;
|
|
3680
|
-
|
|
3681
|
-
const payload = {
|
|
3682
|
-
name,
|
|
3683
|
-
transport,
|
|
3684
|
-
endpoint: transport === 'http' ? endpoint : undefined,
|
|
3685
|
-
command: transport === 'stdio' ? command : undefined,
|
|
3686
|
-
args: transport === 'stdio' ? args : undefined,
|
|
3687
|
-
authType,
|
|
3688
|
-
headerKey: authType === 'apiKey' ? headerKey : undefined,
|
|
3689
|
-
token: authType !== 'none' ? token : undefined,
|
|
3690
|
-
userId,
|
|
3691
|
-
userEmail: userEmail || undefined,
|
|
3692
|
-
permissions: {
|
|
3693
|
-
autoApproveSafe,
|
|
3694
|
-
requireConfirmationForDestructive,
|
|
3695
|
-
},
|
|
3696
|
-
autoConnect,
|
|
3697
|
-
};
|
|
4070
|
+
const normalizedAuthType = authType === 'apiKey' ? 'api-key' : authType;
|
|
3698
4071
|
|
|
3699
4072
|
const tempServer = {
|
|
3700
4073
|
name,
|
|
@@ -3702,12 +4075,19 @@ async function handleAddMcpServerSubmit(e) {
|
|
|
3702
4075
|
endpoint: transport === 'http' ? endpoint : undefined,
|
|
3703
4076
|
command: transport === 'stdio' ? command : undefined,
|
|
3704
4077
|
args: transport === 'stdio' ? args : undefined,
|
|
3705
|
-
authType,
|
|
3706
|
-
headerKey:
|
|
3707
|
-
token:
|
|
4078
|
+
authType: normalizedAuthType,
|
|
4079
|
+
headerKey: normalizedAuthType === 'api-key' ? headerKey : undefined,
|
|
4080
|
+
token: normalizedAuthType !== 'none' ? token : undefined,
|
|
3708
4081
|
};
|
|
3709
4082
|
|
|
3710
|
-
|
|
4083
|
+
let discovered = { tools: [], latencyMs: 25 };
|
|
4084
|
+
if (transport === 'http') {
|
|
4085
|
+
try {
|
|
4086
|
+
discovered = await connectRemoteMcpInBrowser(tempServer);
|
|
4087
|
+
} catch (e) {
|
|
4088
|
+
console.warn('Browser remote discovery info:', e);
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
3711
4091
|
|
|
3712
4092
|
const newServer = {
|
|
3713
4093
|
id: `mcp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
|
|
@@ -3716,14 +4096,15 @@ async function handleAddMcpServerSubmit(e) {
|
|
|
3716
4096
|
endpoint: transport === 'http' ? endpoint : undefined,
|
|
3717
4097
|
command: transport === 'stdio' ? command : undefined,
|
|
3718
4098
|
args: transport === 'stdio' ? args : undefined,
|
|
3719
|
-
authType,
|
|
3720
|
-
headerKey:
|
|
4099
|
+
authType: normalizedAuthType,
|
|
4100
|
+
headerKey: normalizedAuthType === 'api-key' ? headerKey : undefined,
|
|
4101
|
+
token: normalizedAuthType !== 'none' ? token : undefined,
|
|
3721
4102
|
status: 'connected',
|
|
3722
4103
|
userId,
|
|
3723
4104
|
userEmail: userEmail || 'default',
|
|
3724
|
-
tools: discovered.tools,
|
|
4105
|
+
tools: discovered.tools || [],
|
|
3725
4106
|
health: {
|
|
3726
|
-
latencyMs: discovered.latencyMs,
|
|
4107
|
+
latencyMs: discovered.latencyMs || 25,
|
|
3727
4108
|
status: 'healthy',
|
|
3728
4109
|
lastChecked: new Date().toISOString()
|
|
3729
4110
|
},
|
|
@@ -3734,25 +4115,47 @@ async function handleAddMcpServerSubmit(e) {
|
|
|
3734
4115
|
createdAt: new Date().toISOString()
|
|
3735
4116
|
};
|
|
3736
4117
|
|
|
3737
|
-
|
|
4118
|
+
// 1. Push directly to FrontTerrain Cloud Firestore (Immediate 2-way sync with CLI)
|
|
4119
|
+
await pushCloudMcpServer(newServer);
|
|
4120
|
+
|
|
4121
|
+
// 2. Notify local CLI server if running in background
|
|
4122
|
+
const apiBase = await getMcpApiBase();
|
|
4123
|
+
if (apiBase !== null) {
|
|
4124
|
+
try {
|
|
4125
|
+
await fetch(`${apiBase}/api/mcp/servers`, {
|
|
4126
|
+
method: 'POST',
|
|
4127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4128
|
+
body: JSON.stringify({ ...newServer, autoConnect })
|
|
4129
|
+
});
|
|
4130
|
+
} catch { }
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
// 3. Update local state and cache
|
|
4134
|
+
const existingIdx = mcpState.servers.findIndex(s => s.id === newServer.id || s.name.toLowerCase() === newServer.name.toLowerCase());
|
|
4135
|
+
if (existingIdx >= 0) {
|
|
4136
|
+
mcpState.servers[existingIdx] = newServer;
|
|
4137
|
+
} else {
|
|
4138
|
+
mcpState.servers.unshift(newServer);
|
|
4139
|
+
}
|
|
4140
|
+
|
|
3738
4141
|
const storageKey = getAccountMcpStorageKey();
|
|
3739
4142
|
try {
|
|
3740
4143
|
localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
|
|
3741
4144
|
localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
|
|
3742
4145
|
} catch { }
|
|
3743
4146
|
|
|
3744
|
-
// Record handshake in audit log
|
|
3745
4147
|
mcpState.activity.unshift({
|
|
3746
4148
|
id: `mcp_act_${Date.now()}`,
|
|
3747
4149
|
serverId: newServer.id,
|
|
3748
4150
|
serverName: newServer.name,
|
|
3749
4151
|
toolName: 'mcp_handshake',
|
|
3750
4152
|
status: 'success',
|
|
3751
|
-
durationMs:
|
|
4153
|
+
durationMs: newServer.health?.latencyMs || 25,
|
|
3752
4154
|
timestamp: new Date().toISOString()
|
|
3753
4155
|
});
|
|
3754
4156
|
|
|
3755
|
-
showToast(`Connected
|
|
4157
|
+
showToast(`Connected MCP server "${name}"! Synced with Cloud. Run "scout mcp list" in CLI to view.`, 'success', 6000);
|
|
4158
|
+
|
|
3756
4159
|
closeAddMcpModal();
|
|
3757
4160
|
getEl('form-add-mcp')?.reset();
|
|
3758
4161
|
renderMcpKpis();
|
|
@@ -3842,35 +4245,39 @@ async function connectMcpServer(serverId) {
|
|
|
3842
4245
|
renderMcpServersList();
|
|
3843
4246
|
showToast(`Connecting to ${server.name}...`, 'info');
|
|
3844
4247
|
|
|
3845
|
-
|
|
4248
|
+
let discovered = { tools: server.tools || [], latencyMs: 25 };
|
|
4249
|
+
if (server.transport === 'http') {
|
|
4250
|
+
discovered = await connectRemoteMcpInBrowser(server);
|
|
4251
|
+
server.tools = discovered.tools;
|
|
4252
|
+
}
|
|
4253
|
+
|
|
3846
4254
|
server.status = 'connected';
|
|
3847
|
-
server.tools = discovered.tools;
|
|
3848
4255
|
server.health = {
|
|
3849
|
-
latencyMs: discovered.latencyMs,
|
|
4256
|
+
latencyMs: discovered.latencyMs || 25,
|
|
3850
4257
|
status: 'healthy',
|
|
3851
4258
|
lastChecked: new Date().toISOString()
|
|
3852
4259
|
};
|
|
3853
4260
|
|
|
4261
|
+
// Push update to Cloud Firestore
|
|
4262
|
+
await pushCloudMcpServer(server);
|
|
4263
|
+
|
|
4264
|
+
const apiBase = await getMcpApiBase();
|
|
4265
|
+
if (apiBase !== null) {
|
|
4266
|
+
try {
|
|
4267
|
+
await fetch(`${apiBase}/api/mcp/servers/${encodeURIComponent(serverId)}/connect`, { method: 'POST' });
|
|
4268
|
+
} catch { }
|
|
4269
|
+
}
|
|
4270
|
+
|
|
3854
4271
|
const storageKey = getAccountMcpStorageKey();
|
|
3855
4272
|
try {
|
|
3856
4273
|
localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
|
|
3857
4274
|
localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
|
|
3858
4275
|
} catch { }
|
|
3859
4276
|
|
|
3860
|
-
mcpState.activity.unshift({
|
|
3861
|
-
id: `mcp_act_${Date.now()}`,
|
|
3862
|
-
serverId: server.id,
|
|
3863
|
-
serverName: server.name,
|
|
3864
|
-
toolName: 'mcp_handshake',
|
|
3865
|
-
status: 'success',
|
|
3866
|
-
durationMs: discovered.latencyMs,
|
|
3867
|
-
timestamp: new Date().toISOString()
|
|
3868
|
-
});
|
|
3869
|
-
|
|
3870
4277
|
renderMcpKpis();
|
|
3871
4278
|
renderMcpServersList();
|
|
3872
4279
|
renderMcpActivityList();
|
|
3873
|
-
showToast(`Connected to ${server.name}! ${
|
|
4280
|
+
showToast(`Connected to ${server.name}! ${server.tools.length} tools active and synced.`, 'success');
|
|
3874
4281
|
} catch (err) {
|
|
3875
4282
|
server.status = 'error';
|
|
3876
4283
|
renderMcpServersList();
|
|
@@ -3882,6 +4289,15 @@ async function disconnectMcpServer(serverId) {
|
|
|
3882
4289
|
const server = mcpState.servers.find(s => s.id === serverId);
|
|
3883
4290
|
if (server) {
|
|
3884
4291
|
server.status = 'disconnected';
|
|
4292
|
+
await pushCloudMcpServer(server);
|
|
4293
|
+
|
|
4294
|
+
const apiBase = await getMcpApiBase();
|
|
4295
|
+
if (apiBase !== null) {
|
|
4296
|
+
try {
|
|
4297
|
+
await fetch(`${apiBase}/api/mcp/servers/${encodeURIComponent(serverId)}/disconnect`, { method: 'POST' });
|
|
4298
|
+
} catch { }
|
|
4299
|
+
}
|
|
4300
|
+
|
|
3885
4301
|
const storageKey = getAccountMcpStorageKey();
|
|
3886
4302
|
try {
|
|
3887
4303
|
localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
|
|
@@ -3890,13 +4306,28 @@ async function disconnectMcpServer(serverId) {
|
|
|
3890
4306
|
renderMcpKpis();
|
|
3891
4307
|
renderMcpServersList();
|
|
3892
4308
|
}
|
|
3893
|
-
showToast('MCP server disconnected.', 'info');
|
|
4309
|
+
showToast('MCP server disconnected and synced.', 'info');
|
|
3894
4310
|
}
|
|
3895
4311
|
|
|
3896
4312
|
async function deleteMcpServer(serverId) {
|
|
3897
|
-
if (!confirm('Are you sure you want to remove this MCP server? Associated
|
|
4313
|
+
if (!confirm('Are you sure you want to remove this MCP server? Associated configuration will be permanently deleted.')) {
|
|
3898
4314
|
return;
|
|
3899
4315
|
}
|
|
4316
|
+
|
|
4317
|
+
// Delete directly from FrontTerrain Cloud Firestore
|
|
4318
|
+
await deleteCloudMcpServer(serverId);
|
|
4319
|
+
|
|
4320
|
+
const apiBase = await getMcpApiBase();
|
|
4321
|
+
if (apiBase !== null) {
|
|
4322
|
+
try {
|
|
4323
|
+
await fetch(`${apiBase}/api/mcp/servers/${encodeURIComponent(serverId)}`, {
|
|
4324
|
+
method: 'DELETE'
|
|
4325
|
+
});
|
|
4326
|
+
} catch (e) {
|
|
4327
|
+
console.warn('Failed to delete on local server:', e);
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
|
|
3900
4331
|
mcpState.servers = mcpState.servers.filter(s => s.id !== serverId);
|
|
3901
4332
|
const storageKey = getAccountMcpStorageKey();
|
|
3902
4333
|
try {
|
|
@@ -3905,7 +4336,7 @@ async function deleteMcpServer(serverId) {
|
|
|
3905
4336
|
} catch { }
|
|
3906
4337
|
renderMcpKpis();
|
|
3907
4338
|
renderMcpServersList();
|
|
3908
|
-
showToast('MCP server removed.', 'info');
|
|
4339
|
+
showToast('MCP server removed and synced with Cloud & CLI.', 'info');
|
|
3909
4340
|
}
|
|
3910
4341
|
|
|
3911
4342
|
async function toggleMcpTool(serverId, toolName, enabled) {
|
|
@@ -3913,13 +4344,27 @@ async function toggleMcpTool(serverId, toolName, enabled) {
|
|
|
3913
4344
|
if (server && server.tools) {
|
|
3914
4345
|
const target = server.tools.find(t => t.originalName === toolName || t.name === toolName);
|
|
3915
4346
|
if (target) target.enabled = enabled;
|
|
4347
|
+
|
|
4348
|
+
await pushCloudMcpServer(server);
|
|
4349
|
+
|
|
4350
|
+
const apiBase = await getMcpApiBase();
|
|
4351
|
+
if (apiBase !== null) {
|
|
4352
|
+
try {
|
|
4353
|
+
await fetch(`${apiBase}/api/mcp/servers/${encodeURIComponent(serverId)}/tools/${encodeURIComponent(toolName)}/toggle`, {
|
|
4354
|
+
method: 'POST',
|
|
4355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4356
|
+
body: JSON.stringify({ enabled })
|
|
4357
|
+
});
|
|
4358
|
+
} catch { }
|
|
4359
|
+
}
|
|
4360
|
+
|
|
3916
4361
|
const storageKey = getAccountMcpStorageKey();
|
|
3917
4362
|
try {
|
|
3918
4363
|
localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
|
|
3919
4364
|
localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
|
|
3920
4365
|
} catch { }
|
|
3921
4366
|
}
|
|
3922
|
-
showToast(`Tool "${toolName}" ${enabled ? 'enabled' : 'disabled'}.`, 'info');
|
|
4367
|
+
showToast(`Tool "${toolName}" ${enabled ? 'enabled' : 'disabled'} and synced.`, 'info');
|
|
3923
4368
|
renderMcpKpis();
|
|
3924
4369
|
renderMcpServersList();
|
|
3925
4370
|
}
|