ft-scout 9.0.8 → 9.0.9

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/web/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- FrontTerrain Scout Multi-Page Application Engine v9.0.8
2
+ FrontTerrain Scout Multi-Page Application Engine v9.0.9
3
3
  ========================================================================== */
4
4
 
5
5
  const FIREBASE_CONFIG = {
@@ -220,14 +220,28 @@ async function loadDynamicPricing() {
220
220
 
221
221
  try {
222
222
  const adminToken = sessionStorage.getItem('scout_admin_token');
223
- const userEmail = (state.user?.email || '').toLowerCase();
224
- if (adminToken || userEmail === 'rishishandilya40@gmail.com' || userEmail.includes('admin')) {
225
- showAdminNavigationLink();
226
- } else {
227
- const checkRes = await fetch('/api/admin/check');
228
- if (checkRes.ok) {
229
- const checkData = await checkRes.json();
230
- if (checkData.authorized) showAdminNavigationLink();
223
+ const userEmail = (state.user?.email || '').trim().toLowerCase();
224
+ const AUTHORIZED_ADMIN_EMAILS = [
225
+ 'rishishandilya40@gmail.com',
226
+ 'rishi.shandilya@frontterrain.com',
227
+ 'company@frontterrain.com'
228
+ ];
229
+
230
+ if (adminToken && AUTHORIZED_ADMIN_EMAILS.includes(userEmail)) {
231
+ if (state.serverConnected) {
232
+ const checkRes = await fetch('/api/admin/check', {
233
+ headers: { 'Authorization': `Bearer ${adminToken}` }
234
+ });
235
+ if (checkRes.ok) {
236
+ const checkData = await checkRes.json();
237
+ if (checkData.authorized) {
238
+ showAdminNavigationLink();
239
+ } else {
240
+ sessionStorage.removeItem('scout_admin_token');
241
+ }
242
+ }
243
+ } else {
244
+ showAdminNavigationLink();
231
245
  }
232
246
  }
233
247
  } catch { }
@@ -249,7 +263,13 @@ function showAdminNavigationLink() {
249
263
 
250
264
  // Highlight admin identity in sidebar user footer
251
265
  const sidebarTier = document.getElementById('sidebar-user-tier');
252
- if (sidebarTier && (sessionStorage.getItem('scout_admin_token') || (state.user?.email || '').toLowerCase().includes('admin') || (state.user?.email || '').toLowerCase() === 'rishishandilya40@gmail.com')) {
266
+ const userEmail = (state.user?.email || '').trim().toLowerCase();
267
+ const AUTHORIZED_ADMIN_EMAILS = [
268
+ 'rishishandilya40@gmail.com',
269
+ 'rishi.shandilya@frontterrain.com',
270
+ 'company@frontterrain.com'
271
+ ];
272
+ if (sidebarTier && sessionStorage.getItem('scout_admin_token') && AUTHORIZED_ADMIN_EMAILS.includes(userEmail)) {
253
273
  sidebarTier.innerHTML = '<span style="color: #e15b37; font-weight: 700;"><i class="fa-solid fa-shield-halved"></i> Master Admin</span>';
254
274
  }
255
275
 
@@ -1057,7 +1077,7 @@ function initTerminalSimulator() {
1057
1077
  `;
1058
1078
  } else if (lower.includes('status')) {
1059
1079
  response.innerHTML = `
1060
- <span class="term-green">✔ Scout CLI Engine: v9.0.8 Online</span><br>
1080
+ <span class="term-green">✔ Scout CLI Engine: v9.0.9 Online</span><br>
1061
1081
  ↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
1062
1082
  ↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
1063
1083
  ↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
@@ -1304,7 +1324,7 @@ function renderTiersUI() {
1304
1324
  getEl('price-pro-val').innerText = proUSD;
1305
1325
  }
1306
1326
  }
1307
- if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isProOffer ? `/ month (${p.pro?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '(billed annually)' : '/ month');
1327
+ if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isProOffer ? `/ month (${p.pro?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '/month(billed annually)' : '/ month');
1308
1328
  const proCurrency = document.querySelector('#tier-card-pro .price-currency');
1309
1329
  if (proCurrency) proCurrency.innerText = isProOffer ? '' : '$';
1310
1330
 
@@ -1315,7 +1335,7 @@ function renderTiersUI() {
1315
1335
  getEl('price-ent-val').innerText = entUSD;
1316
1336
  }
1317
1337
  }
1318
- if (getEl('price-ent-period')) getEl('price-ent-period').innerText = isEntOffer ? `/ month (${p.enterprise?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '(billed annually)' : '/ month');
1338
+ if (getEl('price-ent-period')) getEl('price-ent-period').innerText = isEntOffer ? `/ month (${p.enterprise?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '/month(billed annually)' : '/ month');
1319
1339
  const entCurrency = document.querySelector('#tier-card-enterprise .price-currency');
1320
1340
  if (entCurrency) entCurrency.innerText = isEntOffer ? '' : '$';
1321
1341
 
@@ -1367,8 +1387,8 @@ async function checkServerStatusAndSession() {
1367
1387
  let connected = false;
1368
1388
  let data = null;
1369
1389
 
1370
- // Only attempt local server if running locally
1371
- const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
1390
+ // Only attempt direct relative path if running locally or via file protocol
1391
+ const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.protocol === 'file:';
1372
1392
 
1373
1393
  if (isLocalhost) {
1374
1394
  try {
@@ -1381,15 +1401,22 @@ async function checkServerStatusAndSession() {
1381
1401
  }
1382
1402
  }
1383
1403
  } catch { }
1404
+ }
1384
1405
 
1385
- if (!connected) {
1406
+ if (!connected) {
1407
+ const candidateHosts = ['http://localhost:9120', 'http://127.0.0.1:9120', 'http://localhost:9121', 'http://localhost:9122'];
1408
+ for (const host of candidateHosts) {
1386
1409
  try {
1387
- const res = await fetch('http://localhost:9120/api/status', { method: 'GET' });
1410
+ const controller = new AbortController();
1411
+ const timeoutId = setTimeout(() => controller.abort(), 1200);
1412
+ const res = await fetch(`${host}/api/status`, { method: 'GET', signal: controller.signal });
1413
+ clearTimeout(timeoutId);
1388
1414
  if (res.ok) {
1389
1415
  data = await res.json();
1390
1416
  if (data && data.status === 'ok') {
1391
- state.apiBaseUrl = 'http://localhost:9120';
1417
+ state.apiBaseUrl = host;
1392
1418
  connected = true;
1419
+ break;
1393
1420
  }
1394
1421
  }
1395
1422
  } catch { }
@@ -1475,8 +1502,46 @@ async function handleSignIn(e) {
1475
1502
  if (!email || !password) return;
1476
1503
 
1477
1504
  // Check Master Admin Credentials
1478
- const isMasterAdminAuth = (email.toLowerCase() === 'rishishandilya40@gmail.com' || email.toLowerCase().includes('admin')) && (password === 'scout_admin_2026' || password === 'admin');
1505
+ const AUTHORIZED_ADMIN_EMAILS = [
1506
+ 'rishishandilya40@gmail.com',
1507
+ 'rishi.shandilya@frontterrain.com',
1508
+ 'company@frontterrain.com'
1509
+ ];
1510
+ const isMasterAdminAuth = AUTHORIZED_ADMIN_EMAILS.includes(email.toLowerCase().trim()) && password === 'scout_admin_2026';
1479
1511
  if (isMasterAdminAuth) {
1512
+ if (state.serverConnected) {
1513
+ try {
1514
+ const adminLoginRes = await fetch(`${state.apiBaseUrl}/api/admin/login`, {
1515
+ method: 'POST',
1516
+ headers: { 'Content-Type': 'application/json' },
1517
+ body: JSON.stringify({ email: email.trim(), passkey: password }),
1518
+ });
1519
+ if (adminLoginRes.ok) {
1520
+ const adminData = await adminLoginRes.json();
1521
+ if (adminData.success && adminData.token) {
1522
+ state.token = adminData.token;
1523
+ state.refreshToken = 'rt_' + Date.now().toString(36);
1524
+ state.user = {
1525
+ id: 'usr_admin_001',
1526
+ email: email,
1527
+ name: 'Scout Administrator',
1528
+ createdAt: new Date().toISOString(),
1529
+ };
1530
+ state.currentTier = 'pro';
1531
+ state.creditsTotal = 1000;
1532
+ state.creditsUsed = 0;
1533
+ sessionStorage.setItem('scout_admin_token', adminData.token);
1534
+ saveSessionToStorage();
1535
+ updateUserUI();
1536
+ showToast('Authenticated as Scout Administrator! Redirecting to Control Center...', 'success');
1537
+ setTimeout(() => { window.location.href = '/admin'; }, 400);
1538
+ return;
1539
+ }
1540
+ }
1541
+ } catch { }
1542
+ }
1543
+
1544
+ // Standalone fallback
1480
1545
  state.token = 'scout_admin_token_' + Date.now().toString(36);
1481
1546
  state.refreshToken = 'rt_' + Date.now().toString(36);
1482
1547
  state.user = {
@@ -1784,6 +1849,11 @@ function updateUserUI() {
1784
1849
  inceptionBadge.style.display = 'none';
1785
1850
  }
1786
1851
  }
1852
+
1853
+ // Refresh MCP servers scoped to active user
1854
+ if (typeof loadMcpData === 'function') {
1855
+ loadMcpData();
1856
+ }
1787
1857
  }
1788
1858
 
1789
1859
  async function handleLogout() {
@@ -1801,6 +1871,11 @@ async function handleLogout() {
1801
1871
  localStorage.removeItem('scout_auth_token');
1802
1872
  localStorage.removeItem('scout_refresh_token');
1803
1873
 
1874
+ if (typeof mcpState !== 'undefined') {
1875
+ mcpState.servers = [];
1876
+ if (typeof renderMcpServersList === 'function') renderMcpServersList();
1877
+ }
1878
+
1804
1879
  showToast('Signed out of Scout session.', 'info');
1805
1880
  window.location.href = '/auth';
1806
1881
  }
@@ -3145,8 +3220,25 @@ function initWorkstationRouter() {
3145
3220
  // Model Context Protocol (MCP) Ecosystem Controller
3146
3221
  // ==========================================================================
3147
3222
 
3223
+ function getMcpApiBase() {
3224
+ return state.apiBaseUrl || '';
3225
+ }
3226
+
3227
+ function getAccountMcpStorageKey() {
3228
+ const email = (state.user?.email || '').trim().toLowerCase();
3229
+ return email ? `scout_mcp_servers_${email}` : 'scout_cached_mcp_servers';
3230
+ }
3231
+
3148
3232
  const mcpState = {
3149
- servers: [],
3233
+ servers: (() => {
3234
+ try {
3235
+ const key = (state.user?.email) ? `scout_mcp_servers_${state.user.email.toLowerCase()}` : 'scout_cached_mcp_servers';
3236
+ const cached = localStorage.getItem(key) || localStorage.getItem('scout_cached_mcp_servers');
3237
+ return cached ? JSON.parse(cached) : [];
3238
+ } catch {
3239
+ return [];
3240
+ }
3241
+ })(),
3150
3242
  activity: [],
3151
3243
  health: {},
3152
3244
  selectedServer: null,
@@ -3154,21 +3246,69 @@ const mcpState = {
3154
3246
  };
3155
3247
 
3156
3248
  async function loadMcpData() {
3157
- try {
3158
- const [serversRes, healthRes, activityRes] = await Promise.all([
3159
- fetch('/api/mcp/servers').then(r => r.json()).catch(() => ({ success: false, servers: [] })),
3160
- fetch('/api/mcp/health').then(r => r.json()).catch(() => ({ success: false, health: {} })),
3161
- fetch('/api/mcp/activity?limit=30').then(r => r.json()).catch(() => ({ success: false, activity: [] }))
3162
- ]);
3249
+ const userEmail = (state.user?.email || '').trim().toLowerCase();
3250
+ const storageKey = getAccountMcpStorageKey();
3163
3251
 
3164
- if (serversRes.success && Array.isArray(serversRes.servers)) {
3165
- mcpState.servers = serversRes.servers;
3252
+ // 1. Immediately display cached servers for this account
3253
+ try {
3254
+ const cached = localStorage.getItem(storageKey);
3255
+ if (cached) {
3256
+ const parsed = JSON.parse(cached);
3257
+ if (Array.isArray(parsed) && parsed.length > 0) {
3258
+ mcpState.servers = parsed;
3259
+ renderMcpKpis();
3260
+ renderMcpServersList();
3261
+ }
3166
3262
  }
3167
- if (healthRes.success && healthRes.health) {
3168
- mcpState.health = healthRes.health;
3263
+ } catch { }
3264
+
3265
+ // 2. Update account badge in header
3266
+ const badgeEl = getEl('mcp-account-badge');
3267
+ if (badgeEl) {
3268
+ if (state.user?.email) {
3269
+ badgeEl.style.display = 'inline-flex';
3270
+ badgeEl.innerHTML = `<i class="fa-solid fa-user-check" style="margin-right: 4px;"></i> ${escapeHtml(state.user.email)}`;
3271
+ } else {
3272
+ badgeEl.style.display = 'none';
3169
3273
  }
3170
- if (activityRes.success && Array.isArray(activityRes.activity)) {
3171
- mcpState.activity = activityRes.activity;
3274
+ }
3275
+
3276
+ try {
3277
+ const apiBase = getMcpApiBase();
3278
+ const canFetchServer = state.serverConnected || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || (apiBase && apiBase.length > 0);
3279
+
3280
+ if (canFetchServer) {
3281
+ const queryParams = userEmail ? `?user=${encodeURIComponent(userEmail)}` : '';
3282
+ const [serversRes, healthRes, activityRes] = await Promise.all([
3283
+ fetch(`${apiBase}/api/mcp/servers${queryParams}`).then(r => r.json()).catch(() => ({ success: false, servers: [] })),
3284
+ fetch(`${apiBase}/api/mcp/health`).then(r => r.json()).catch(() => ({ success: false, health: {} })),
3285
+ fetch(`${apiBase}/api/mcp/activity?limit=30`).then(r => r.json()).catch(() => ({ success: false, activity: [] }))
3286
+ ]);
3287
+
3288
+ if (serversRes.success && Array.isArray(serversRes.servers)) {
3289
+ mcpState.servers = serversRes.servers;
3290
+ try {
3291
+ localStorage.setItem(storageKey, JSON.stringify(serversRes.servers));
3292
+ localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(serversRes.servers));
3293
+ } catch { }
3294
+ }
3295
+ if (healthRes.success && healthRes.health) {
3296
+ mcpState.health = healthRes.health;
3297
+ }
3298
+ if (activityRes.success && Array.isArray(activityRes.activity)) {
3299
+ mcpState.activity = activityRes.activity;
3300
+ }
3301
+ } else {
3302
+ // Standalone Cloud Web Mode (e.g. on scout.frontterrain.com without local server online)
3303
+ try {
3304
+ const stored = localStorage.getItem(storageKey);
3305
+ if (stored) {
3306
+ mcpState.servers = JSON.parse(stored);
3307
+ } else if (!userEmail) {
3308
+ const fallback = localStorage.getItem('scout_cached_mcp_servers');
3309
+ if (fallback) mcpState.servers = JSON.parse(fallback);
3310
+ }
3311
+ } catch { }
3172
3312
  }
3173
3313
 
3174
3314
  renderMcpKpis();
@@ -3221,6 +3361,32 @@ function renderMcpServersList() {
3221
3361
  if (!container) return;
3222
3362
 
3223
3363
  if (mcpState.servers.length === 0) {
3364
+ if (!state.serverConnected) {
3365
+ container.innerHTML = `
3366
+ <div style="padding: 36px 24px; text-align: center; border: 1px dashed rgba(0, 229, 255, 0.3); border-radius: 12px; background: rgba(0, 229, 255, 0.03); margin: 16px;">
3367
+ <div style="width: 52px; height: 52px; border-radius: 12px; background: rgba(0, 229, 255, 0.12); border: 1px solid rgba(0, 229, 255, 0.25); color: #00e5ff; display: inline-flex; align-items: center; justify-content: center; font-size: 22px; margin-bottom: 14px;">
3368
+ <i class="fa-solid fa-cubes-stacked"></i>
3369
+ </div>
3370
+ <h4 style="font-size: 16px; font-weight: 700; color: #fff; margin: 0 0 8px 0;">No External MCP Servers Configured</h4>
3371
+ <p style="font-size: 13px; color: var(--scout-text-secondary); max-width: 520px; margin: 0 auto 16px auto; line-height: 1.5;">
3372
+ Connect external Model Context Protocol (MCP) servers to grant autonomous tools to Scout under <strong>${escapeHtml(state.user?.email || 'this account')}</strong>.
3373
+ </p>
3374
+ <div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px;">
3375
+ <button type="button" class="btn btn-sm btn-primary" onclick="openAddMcpModal()">
3376
+ <i class="fa-solid fa-plus"></i> Connect First MCP Server
3377
+ </button>
3378
+ <button type="button" class="btn btn-sm btn-outline" onclick="checkServerStatusAndSession().then(loadMcpData)">
3379
+ <i class="fa-solid fa-satellite-dish"></i> Sync with Local CLI Server
3380
+ </button>
3381
+ </div>
3382
+ <div style="font-size: 11px; color: var(--scout-text-dim);">
3383
+ <i class="fa-solid fa-terminal" style="color: #00e5ff;"></i> Tip: Run <code>scout dashboard</code> in your terminal for real-time 2-way CLI sync.
3384
+ </div>
3385
+ </div>
3386
+ `;
3387
+ return;
3388
+ }
3389
+
3224
3390
  container.innerHTML = `
3225
3391
  <div style="padding: 48px 24px; text-align: center;">
3226
3392
  <div style="width: 56px; height: 56px; border-radius: 14px; background: rgba(0, 229, 255, 0.08); border: 1px solid rgba(0, 229, 255, 0.2); color: #00e5ff; display: inline-flex; align-items: center; justify-content: center; font-size: 24px; margin-bottom: 16px;">
@@ -3454,6 +3620,9 @@ async function handleAddMcpServerSubmit(e) {
3454
3620
  }
3455
3621
 
3456
3622
  try {
3623
+ const userEmail = (state.user?.email || '').trim().toLowerCase();
3624
+ const userId = state.user?.id || state.user?.uid || undefined;
3625
+
3457
3626
  const payload = {
3458
3627
  name,
3459
3628
  transport,
@@ -3463,6 +3632,8 @@ async function handleAddMcpServerSubmit(e) {
3463
3632
  authType,
3464
3633
  headerKey: authType === 'apiKey' ? headerKey : undefined,
3465
3634
  token: authType !== 'none' ? token : undefined,
3635
+ userId,
3636
+ userEmail: userEmail || undefined,
3466
3637
  permissions: {
3467
3638
  autoApproveSafe,
3468
3639
  requireConfirmationForDestructive,
@@ -3470,20 +3641,51 @@ async function handleAddMcpServerSubmit(e) {
3470
3641
  autoConnect,
3471
3642
  };
3472
3643
 
3473
- const res = await fetch('/api/mcp/servers', {
3474
- method: 'POST',
3475
- headers: { 'Content-Type': 'application/json' },
3476
- body: JSON.stringify(payload)
3477
- });
3644
+ const apiBase = getMcpApiBase();
3645
+ const canCallApi = state.serverConnected || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || (apiBase && apiBase.length > 0);
3478
3646
 
3479
- const data = await res.json();
3480
- if (data.success) {
3481
- showToast(`Connected to MCP server "${name}"! Discovered ${data.server.tools?.length || 0} tools.`, 'success');
3647
+ if (canCallApi) {
3648
+ const res = await fetch(`${apiBase}/api/mcp/servers`, {
3649
+ method: 'POST',
3650
+ headers: { 'Content-Type': 'application/json' },
3651
+ body: JSON.stringify(payload)
3652
+ });
3653
+
3654
+ const data = await res.json();
3655
+ if (data.success) {
3656
+ showToast(`Connected to MCP server "${name}"! Discovered ${data.server?.tools?.length || 0} tools.`, 'success');
3657
+ closeAddMcpModal();
3658
+ getEl('form-add-mcp')?.reset();
3659
+ await loadMcpData();
3660
+ } else {
3661
+ showToast(data.error || 'Failed to add MCP server', 'error');
3662
+ }
3663
+ } else {
3664
+ // Standalone Cloud Web Mode fallback on scout.frontterrain.com
3665
+ const newServer = {
3666
+ id: `mcp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
3667
+ name,
3668
+ transport,
3669
+ endpoint: transport === 'http' ? endpoint : undefined,
3670
+ command: transport === 'stdio' ? command : undefined,
3671
+ args: transport === 'stdio' ? args : undefined,
3672
+ authType,
3673
+ headerKey: authType === 'apiKey' ? headerKey : undefined,
3674
+ status: transport === 'http' ? 'connected' : 'disconnected',
3675
+ userId,
3676
+ userEmail: userEmail || 'default',
3677
+ tools: [],
3678
+ createdAt: new Date().toISOString()
3679
+ };
3680
+ mcpState.servers.push(newServer);
3681
+ const storageKey = getAccountMcpStorageKey();
3682
+ localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
3683
+ localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
3684
+ showToast(`MCP server "${name}" saved for ${state.user?.email || 'your account'}.`, 'success');
3482
3685
  closeAddMcpModal();
3483
3686
  getEl('form-add-mcp')?.reset();
3484
- await loadMcpData();
3485
- } else {
3486
- showToast(data.error || 'Failed to add MCP server', 'error');
3687
+ renderMcpKpis();
3688
+ renderMcpServersList();
3487
3689
  }
3488
3690
  } catch (err) {
3489
3691
  showToast(err.message || 'Error connecting MCP server', 'error');
@@ -3560,7 +3762,7 @@ function closeInspectMcpModal() {
3560
3762
  async function connectMcpServer(serverId) {
3561
3763
  try {
3562
3764
  showToast('Connecting to MCP server...', 'info');
3563
- const res = await fetch(`/api/mcp/servers/${encodeURIComponent(serverId)}/connect`, { method: 'POST' });
3765
+ const res = await fetch(`${getMcpApiBase()}/api/mcp/servers/${encodeURIComponent(serverId)}/connect`, { method: 'POST' });
3564
3766
  const data = await res.json();
3565
3767
  if (data.success) {
3566
3768
  showToast(`Connected! Discovered ${data.server.tools?.length || 0} tools.`, 'success');
@@ -3575,7 +3777,7 @@ async function connectMcpServer(serverId) {
3575
3777
 
3576
3778
  async function disconnectMcpServer(serverId) {
3577
3779
  try {
3578
- const res = await fetch(`/api/mcp/servers/${encodeURIComponent(serverId)}/disconnect`, { method: 'POST' });
3780
+ const res = await fetch(`${getMcpApiBase()}/api/mcp/servers/${encodeURIComponent(serverId)}/disconnect`, { method: 'POST' });
3579
3781
  const data = await res.json();
3580
3782
  if (data.success) {
3581
3783
  showToast('MCP server disconnected.', 'info');
@@ -3593,14 +3795,29 @@ async function deleteMcpServer(serverId) {
3593
3795
  return;
3594
3796
  }
3595
3797
  try {
3596
- const res = await fetch(`/api/mcp/servers/${encodeURIComponent(serverId)}`, { method: 'DELETE' });
3597
- const data = await res.json();
3598
- if (data.success) {
3599
- showToast('MCP server removed.', 'info');
3600
- await loadMcpData();
3798
+ const apiBase = getMcpApiBase();
3799
+ const canCallApi = state.serverConnected || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || (apiBase && apiBase.length > 0);
3800
+
3801
+ if (canCallApi) {
3802
+ const res = await fetch(`${apiBase}/api/mcp/servers/${encodeURIComponent(serverId)}`, { method: 'DELETE' });
3803
+ const data = await res.json();
3804
+ if (data.success) {
3805
+ showToast('MCP server removed.', 'info');
3806
+ } else {
3807
+ showToast(data.error || 'Failed to delete server', 'error');
3808
+ }
3601
3809
  } else {
3602
- showToast(data.error || 'Failed to delete server', 'error');
3810
+ showToast('MCP server removed from your account.', 'info');
3603
3811
  }
3812
+
3813
+ mcpState.servers = mcpState.servers.filter(s => s.id !== serverId);
3814
+ const storageKey = getAccountMcpStorageKey();
3815
+ try {
3816
+ localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
3817
+ localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
3818
+ } catch { }
3819
+ renderMcpKpis();
3820
+ renderMcpServersList();
3604
3821
  } catch (err) {
3605
3822
  showToast(err.message || 'Error deleting server', 'error');
3606
3823
  }
@@ -3608,7 +3825,7 @@ async function deleteMcpServer(serverId) {
3608
3825
 
3609
3826
  async function toggleMcpTool(serverId, toolName, enabled) {
3610
3827
  try {
3611
- const res = await fetch(`/api/mcp/servers/${encodeURIComponent(serverId)}/tools/${encodeURIComponent(toolName)}/toggle`, {
3828
+ const res = await fetch(`${getMcpApiBase()}/api/mcp/servers/${encodeURIComponent(serverId)}/tools/${encodeURIComponent(toolName)}/toggle`, {
3612
3829
  method: 'POST',
3613
3830
  headers: { 'Content-Type': 'application/json' },
3614
3831
  body: JSON.stringify({ enabled })
@@ -3631,6 +3848,11 @@ async function toggleMcpTool(serverId, toolName, enabled) {
3631
3848
  }
3632
3849
  }
3633
3850
 
3851
+ // Auto-sync when window regains focus (e.g., returning from CLI terminal)
3852
+ window.addEventListener('focus', () => {
3853
+ loadMcpData();
3854
+ });
3855
+
3634
3856
  // Global exports for inline HTML event handlers
3635
3857
  window.loadMcpData = loadMcpData;
3636
3858
  window.openAddMcpModal = openAddMcpModal;
package/web/styles.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- FrontTerrain Scout Design System & Visual Engine v9.0.8
2
+ FrontTerrain Scout Design System & Visual Engine v9.0.9
3
3
  Inspired by HappyRobot, Linear.app & Vercel aesthetics
4
4
  ========================================================================== */
5
5
 
@@ -1055,7 +1055,7 @@ h1, h2, h3, h4, h5, h6 {
1055
1055
  .footer-links a:hover { color: var(--hr-rust); }
1056
1056
 
1057
1057
  /* ==========================================================================
1058
- Visual Perception & Automation Engine v9.0.8
1058
+ Visual Perception & Automation Engine v9.0.9
1059
1059
  ========================================================================== */
1060
1060
  .vision-banner {
1061
1061
  margin-bottom: 28px;