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.
Files changed (53) hide show
  1. package/.firebase/hosting.d2Vi.cache +9 -9
  2. package/.mcp-config.json +19 -0
  3. package/.mft-mcp-config.json +29 -0
  4. package/README.md +2 -2
  5. package/bin/src/commands/authCommand.d.ts.map +1 -1
  6. package/bin/src/commands/authCommand.js +13 -2
  7. package/bin/src/commands/authCommand.js.map +1 -1
  8. package/bin/src/commands/login.d.ts.map +1 -1
  9. package/bin/src/commands/login.js +39 -6
  10. package/bin/src/commands/login.js.map +1 -1
  11. package/bin/src/commands/logout.d.ts.map +1 -1
  12. package/bin/src/commands/logout.js +67 -2
  13. package/bin/src/commands/logout.js.map +1 -1
  14. package/bin/src/commands/mcp.d.ts.map +1 -1
  15. package/bin/src/commands/mcp.js +6 -1
  16. package/bin/src/commands/mcp.js.map +1 -1
  17. package/bin/src/commands/switchCommand.d.ts.map +1 -0
  18. package/bin/src/commands/switchCommand.js +178 -0
  19. package/bin/src/commands/switchCommand.js.map +1 -0
  20. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  21. package/bin/src/engine/agentEngine.js +24 -12
  22. package/bin/src/engine/agentEngine.js.map +1 -1
  23. package/bin/src/engine/toolRegistry.d.ts.map +1 -1
  24. package/bin/src/engine/toolRegistry.js +1 -0
  25. package/bin/src/engine/toolRegistry.js.map +1 -1
  26. package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -1
  27. package/bin/src/engine/workspace/agentWorkspace.js +6 -4
  28. package/bin/src/engine/workspace/agentWorkspace.js.map +1 -1
  29. package/bin/src/index.js +28 -9
  30. package/bin/src/index.js.map +1 -1
  31. package/bin/src/mcp/client.js +1 -1
  32. package/bin/src/mcp/cloudSync.d.ts.map +1 -0
  33. package/bin/src/mcp/cloudSync.js +109 -0
  34. package/bin/src/mcp/cloudSync.js.map +1 -0
  35. package/bin/src/mcp/index.d.ts.map +1 -1
  36. package/bin/src/mcp/index.js +1 -0
  37. package/bin/src/mcp/index.js.map +1 -1
  38. package/bin/src/mcp/manager.d.ts.map +1 -1
  39. package/bin/src/mcp/manager.js +40 -1
  40. package/bin/src/mcp/manager.js.map +1 -1
  41. package/bin/src/server/server.d.ts.map +1 -1
  42. package/bin/src/server/server.js +89 -9
  43. package/bin/src/server/server.js.map +1 -1
  44. package/bin/src/utils/auth.d.ts.map +1 -1
  45. package/bin/src/utils/auth.js +211 -0
  46. package/bin/src/utils/auth.js.map +1 -1
  47. package/bin/src/utils/branding.js +1 -1
  48. package/bin/src/utils/qrBridge.d.ts.map +1 -0
  49. package/bin/src/utils/qrBridge.js +216 -0
  50. package/bin/src/utils/qrBridge.js.map +1 -0
  51. package/package.json +2 -2
  52. package/web/app.js +497 -52
  53. 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.0
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.2.0 Online</span><br>
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`, { method: 'POST' });
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: authType === 'apiKey' ? headerKey : undefined,
3707
- token: authType !== 'none' ? token : undefined,
4078
+ authType: normalizedAuthType,
4079
+ headerKey: normalizedAuthType === 'api-key' ? headerKey : undefined,
4080
+ token: normalizedAuthType !== 'none' ? token : undefined,
3708
4081
  };
3709
4082
 
3710
- const discovered = await connectRemoteMcpInBrowser(tempServer);
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: authType === 'apiKey' ? headerKey : undefined,
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
- mcpState.servers.push(newServer);
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: discovered.latencyMs,
4153
+ durationMs: newServer.health?.latencyMs || 25,
3752
4154
  timestamp: new Date().toISOString()
3753
4155
  });
3754
4156
 
3755
- showToast(`Connected to MCP server "${name}"! Click "Copy CLI Command" on the card to run it in your terminal.`, 'success', 6000);
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
- const discovered = await connectRemoteMcpInBrowser(server);
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}! ${discovered.tools.length} tools active.`, 'success');
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 credentials will be permanently deleted.')) {
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
  }