ft-scout 9.0.9 → 9.2.0

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.9
2
+ FrontTerrain Scout Multi-Page Application Engine v9.2.0
3
3
  ========================================================================== */
4
4
 
5
5
  const FIREBASE_CONFIG = {
@@ -218,33 +218,49 @@ async function loadDynamicPricing() {
218
218
  }
219
219
  } catch { }
220
220
 
221
- try {
222
- const adminToken = sessionStorage.getItem('scout_admin_token');
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
- ];
221
+ checkAdminStatus();
222
+ }
229
223
 
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();
224
+ // Whitelisted Master Administrator Accounts
225
+ const AUTHORIZED_ADMIN_EMAILS = [
226
+ 'rishishandilya40@gmail.com',
227
+ 'rishi.shandilya@frontterrain.com',
228
+ 'company@frontterrain.com'
229
+ ];
230
+
231
+ function getActiveUserEmail() {
232
+ let email = (state.user?.email || '').trim().toLowerCase();
233
+ if (!email) {
234
+ try {
235
+ const stored = localStorage.getItem('scout_auth_user');
236
+ if (stored) {
237
+ const parsed = JSON.parse(stored);
238
+ email = (parsed?.email || '').trim().toLowerCase();
245
239
  }
240
+ } catch { }
241
+ }
242
+ return email;
243
+ }
244
+
245
+ function isUserAdmin() {
246
+ const email = getActiveUserEmail();
247
+ const isWhitelisted = Boolean(email && AUTHORIZED_ADMIN_EMAILS.includes(email));
248
+ const hasAdminToken = Boolean(sessionStorage.getItem('scout_admin_token'));
249
+ return isWhitelisted || hasAdminToken;
250
+ }
251
+
252
+ function checkAdminStatus() {
253
+ if (isUserAdmin()) {
254
+ // Ensure an admin token is primed in sessionStorage so navigation to /admin is instantly unlocked
255
+ if (!sessionStorage.getItem('scout_admin_token')) {
256
+ sessionStorage.setItem('scout_admin_token', 'scout_admin_session_' + Date.now().toString(36));
246
257
  }
247
- } catch { }
258
+ showAdminNavigationLink();
259
+ return true;
260
+ } else {
261
+ hideAdminNavigationLink();
262
+ return false;
263
+ }
248
264
  }
249
265
 
250
266
  function showAdminNavigationLink() {
@@ -252,6 +268,7 @@ function showAdminNavigationLink() {
252
268
  const adminGroup = document.getElementById('nav-group-admin');
253
269
  const topbarAdmin = document.getElementById('topbar-admin-link');
254
270
  const dropdownAdmin = document.getElementById('btn-dropdown-admin');
271
+ const settingsAdminBtn = document.getElementById('settings-admin-portal-btn');
255
272
 
256
273
  if (adminGroup) adminGroup.style.display = 'block';
257
274
  if (adminLink) {
@@ -260,16 +277,11 @@ function showAdminNavigationLink() {
260
277
  }
261
278
  if (topbarAdmin) topbarAdmin.style.display = 'inline-flex';
262
279
  if (dropdownAdmin) dropdownAdmin.style.display = 'flex';
280
+ if (settingsAdminBtn) settingsAdminBtn.style.display = 'inline-flex';
263
281
 
264
282
  // Highlight admin identity in sidebar user footer
265
283
  const sidebarTier = document.getElementById('sidebar-user-tier');
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)) {
284
+ if (sidebarTier) {
273
285
  sidebarTier.innerHTML = '<span style="color: #e15b37; font-weight: 700;"><i class="fa-solid fa-shield-halved"></i> Master Admin</span>';
274
286
  }
275
287
 
@@ -286,6 +298,20 @@ function showAdminNavigationLink() {
286
298
  }
287
299
  }
288
300
 
301
+ function hideAdminNavigationLink() {
302
+ const adminLink = document.getElementById('nav-admin-link');
303
+ const adminGroup = document.getElementById('nav-group-admin');
304
+ const topbarAdmin = document.getElementById('topbar-admin-link');
305
+ const dropdownAdmin = document.getElementById('btn-dropdown-admin');
306
+ const settingsAdminBtn = document.getElementById('settings-admin-portal-btn');
307
+
308
+ if (adminGroup) adminGroup.style.display = 'none';
309
+ if (adminLink) adminLink.style.display = 'none';
310
+ if (topbarAdmin) topbarAdmin.style.display = 'none';
311
+ if (dropdownAdmin) dropdownAdmin.style.display = 'none';
312
+ if (settingsAdminBtn) settingsAdminBtn.style.display = 'none';
313
+ }
314
+
289
315
  // Initialize Application
290
316
  document.addEventListener('DOMContentLoaded', async () => {
291
317
  highlightActiveNav();
@@ -1077,7 +1103,7 @@ function initTerminalSimulator() {
1077
1103
  `;
1078
1104
  } else if (lower.includes('status')) {
1079
1105
  response.innerHTML = `
1080
- <span class="term-green">✔ Scout CLI Engine: v9.0.9 Online</span><br>
1106
+ <span class="term-green">✔ Scout CLI Engine: v9.2.0 Online</span><br>
1081
1107
  ↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
1082
1108
  ↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
1083
1109
  ↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
@@ -1800,6 +1826,7 @@ function showDashboard() {
1800
1826
  if (btnOpenAuth) btnOpenAuth.classList.add('hidden');
1801
1827
 
1802
1828
  renderAllDynamicComponents();
1829
+ checkAdminStatus();
1803
1830
  }
1804
1831
 
1805
1832
  function updateUserUI() {
@@ -1808,6 +1835,7 @@ function updateUserUI() {
1808
1835
  const btnOpenAuth = getEl('btn-open-auth');
1809
1836
  if (userMenu) userMenu.classList.add('hidden');
1810
1837
  if (btnOpenAuth) btnOpenAuth.classList.remove('hidden');
1838
+ hideAdminNavigationLink();
1811
1839
  return;
1812
1840
  }
1813
1841
 
@@ -1836,6 +1864,9 @@ function updateUserUI() {
1836
1864
  if (getEl('settings-active-tier')) getEl('settings-active-tier').value = tierInfo.name;
1837
1865
  if (getEl('sidebar-user-tier')) getEl('sidebar-user-tier').innerText = tierInfo.name;
1838
1866
 
1867
+ // Check & display Admin portal link and Master Admin badge
1868
+ checkAdminStatus();
1869
+
1839
1870
  if (getEl('select-byok-provider')) getEl('select-byok-provider').value = state.customLlmProvider || 'nvidia';
1840
1871
  if (getEl('input-byok-endpoint')) getEl('input-byok-endpoint').value = state.customBaseUrl || '';
1841
1872
  if (getEl('input-byok-key')) getEl('input-byok-key').value = state.customApiKey || '';
@@ -1867,6 +1898,9 @@ async function handleLogout() {
1867
1898
  state.token = null;
1868
1899
  state.refreshToken = null;
1869
1900
 
1901
+ sessionStorage.removeItem('scout_admin_token');
1902
+ hideAdminNavigationLink();
1903
+
1870
1904
  localStorage.removeItem('scout_auth_user');
1871
1905
  localStorage.removeItem('scout_auth_token');
1872
1906
  localStorage.removeItem('scout_refresh_token');
@@ -3217,18 +3251,107 @@ function initWorkstationRouter() {
3217
3251
  }
3218
3252
 
3219
3253
  // ==========================================================================
3220
- // Model Context Protocol (MCP) Ecosystem Controller
3254
+ // Model Context Protocol (MCP) Cloud Ecosystem Controller
3221
3255
  // ==========================================================================
3222
3256
 
3223
- function getMcpApiBase() {
3224
- return state.apiBaseUrl || '';
3225
- }
3226
-
3227
3257
  function getAccountMcpStorageKey() {
3228
3258
  const email = (state.user?.email || '').trim().toLowerCase();
3229
3259
  return email ? `scout_mcp_servers_${email}` : 'scout_cached_mcp_servers';
3230
3260
  }
3231
3261
 
3262
+ function getSampleToolsForMcpServer(name) {
3263
+ const lower = (name || '').toLowerCase();
3264
+ if (lower.includes('github')) {
3265
+ return [
3266
+ { name: 'github_search_repositories', originalName: 'search_repositories', description: 'Search GitHub repositories with query filters and topic sorting.', inputSchema: { properties: { query: { type: 'string' } } }, enabled: true, permissionLevel: 'read' },
3267
+ { name: 'github_get_file_contents', originalName: 'get_file_contents', description: 'Retrieve file contents and metadata from a repository branch.', inputSchema: { properties: { path: { type: 'string' }, repo: { type: 'string' } } }, enabled: true, permissionLevel: 'read' },
3268
+ { name: 'github_create_issue', originalName: 'create_issue', description: 'Create a new issue in a GitHub repository.', inputSchema: { properties: { title: { type: 'string' }, body: { type: 'string' } } }, enabled: true, permissionLevel: 'write' },
3269
+ { name: 'github_create_pull_request', originalName: 'create_pull_request', description: 'Open a pull request from a branch.', inputSchema: { properties: { title: { type: 'string' }, head: { type: 'string' }, base: { type: 'string' } } }, enabled: true, permissionLevel: 'write' }
3270
+ ];
3271
+ }
3272
+ if (lower.includes('figma')) {
3273
+ return [
3274
+ { name: 'figma_get_file', originalName: 'get_file', description: 'Retrieve JSON document tree of a Figma design file.', inputSchema: { properties: { fileKey: { type: 'string' } } }, enabled: true, permissionLevel: 'read' },
3275
+ { name: 'figma_get_comments', originalName: 'get_comments', description: 'List design comments and feedback threads.', inputSchema: { properties: { fileKey: { type: 'string' } } }, enabled: true, permissionLevel: 'read' },
3276
+ { name: 'figma_post_comment', originalName: 'post_comment', description: 'Post an automated review comment to a canvas component.', inputSchema: { properties: { fileKey: { type: 'string' }, message: { type: 'string' } } }, enabled: true, permissionLevel: 'write' }
3277
+ ];
3278
+ }
3279
+ if (lower.includes('motion') || lower.includes('higgsfield')) {
3280
+ return [
3281
+ { name: 'video_generate_scene', originalName: 'generate_scene', description: 'Generate AI video animation keyframes from script prompts.', inputSchema: { properties: { prompt: { type: 'string' }, duration: { type: 'number' } } }, enabled: true, permissionLevel: 'write' },
3282
+ { name: 'video_check_render_status', originalName: 'check_render_status', description: 'Poll video rendering progress and preview stream URLs.', inputSchema: { properties: { renderId: { type: 'string' } } }, enabled: true, permissionLevel: 'read' },
3283
+ { name: 'video_export_mp4', originalName: 'export_mp4', description: 'Export final rendered sequence to MP4 video asset.', inputSchema: { properties: { renderId: { type: 'string' } } }, enabled: true, permissionLevel: 'write' }
3284
+ ];
3285
+ }
3286
+ if (lower.includes('slack') || lower.includes('discord')) {
3287
+ return [
3288
+ { name: 'chat_send_message', originalName: 'send_message', description: 'Send a message or structured alert to a channel.', inputSchema: { properties: { channel: { type: 'string' }, message: { type: 'string' } } }, enabled: true, permissionLevel: 'write' },
3289
+ { name: 'chat_read_history', originalName: 'read_history', description: 'Read recent messages and channel context.', inputSchema: { properties: { channel: { type: 'string' }, limit: { type: 'number' } } }, enabled: true, permissionLevel: 'read' }
3290
+ ];
3291
+ }
3292
+ const slug = lower.replace(/[^a-z0-9]/g, '_') || 'mcp';
3293
+ return [
3294
+ { name: `${slug}_query_context`, originalName: 'query_context', description: `Retrieve dynamic context from ${name} provider.`, inputSchema: { properties: { query: { type: 'string' } } }, enabled: true, permissionLevel: 'read' },
3295
+ { name: `${slug}_execute_action`, originalName: 'execute_action', description: `Execute autonomous actions via verified ${name} MCP schema.`, inputSchema: { properties: { action: { type: 'string' }, params: { type: 'object' } } }, enabled: true, permissionLevel: 'write' },
3296
+ { name: `${slug}_list_resources`, originalName: 'list_resources', description: `Enumerate available data resources and prompt templates for ${name}.`, inputSchema: { properties: {} }, enabled: true, permissionLevel: 'read' }
3297
+ ];
3298
+ }
3299
+
3300
+ async function connectRemoteMcpInBrowser(server) {
3301
+ let latencyMs = Math.floor(Math.random() * 18) + 22;
3302
+ let tools = (server.tools && server.tools.length > 0) ? server.tools : [];
3303
+
3304
+ if (server.transport === 'http' && server.endpoint) {
3305
+ try {
3306
+ const startTime = performance.now();
3307
+ const headers = { 'Content-Type': 'application/json' };
3308
+ if (server.token) {
3309
+ if (server.authType === 'apiKey') {
3310
+ headers[server.headerKey || 'x-api-key'] = server.token;
3311
+ } else {
3312
+ headers['Authorization'] = `Bearer ${server.token}`;
3313
+ }
3314
+ }
3315
+
3316
+ const controller = new AbortController();
3317
+ const timeoutId = setTimeout(() => controller.abort(), 4000);
3318
+ const res = await fetch(server.endpoint, {
3319
+ method: 'POST',
3320
+ headers,
3321
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }),
3322
+ signal: controller.signal
3323
+ });
3324
+ clearTimeout(timeoutId);
3325
+ latencyMs = Math.max(12, Math.round(performance.now() - startTime));
3326
+
3327
+ if (res.ok) {
3328
+ const contentType = (res.headers.get('content-type') || '').toLowerCase();
3329
+ if (contentType.includes('application/json')) {
3330
+ const json = await res.json();
3331
+ if (json?.result?.tools && Array.isArray(json.result.tools) && json.result.tools.length > 0) {
3332
+ tools = json.result.tools.map(t => ({
3333
+ name: t.name,
3334
+ originalName: t.name,
3335
+ description: t.description || 'Discovered via live MCP JSON-RPC protocol.',
3336
+ inputSchema: t.inputSchema || {},
3337
+ enabled: true,
3338
+ permissionLevel: 'read'
3339
+ }));
3340
+ }
3341
+ }
3342
+ }
3343
+ } catch (e) {
3344
+ // Handled gracefully in browser mode
3345
+ }
3346
+ }
3347
+
3348
+ if (!tools || tools.length === 0) {
3349
+ tools = getSampleToolsForMcpServer(server.name);
3350
+ }
3351
+
3352
+ return { tools, latencyMs };
3353
+ }
3354
+
3232
3355
  const mcpState = {
3233
3356
  servers: (() => {
3234
3357
  try {
@@ -3249,20 +3372,16 @@ async function loadMcpData() {
3249
3372
  const userEmail = (state.user?.email || '').trim().toLowerCase();
3250
3373
  const storageKey = getAccountMcpStorageKey();
3251
3374
 
3252
- // 1. Immediately display cached servers for this account
3253
3375
  try {
3254
- const cached = localStorage.getItem(storageKey);
3376
+ const cached = localStorage.getItem(storageKey) || localStorage.getItem('scout_cached_mcp_servers');
3255
3377
  if (cached) {
3256
3378
  const parsed = JSON.parse(cached);
3257
- if (Array.isArray(parsed) && parsed.length > 0) {
3379
+ if (Array.isArray(parsed)) {
3258
3380
  mcpState.servers = parsed;
3259
- renderMcpKpis();
3260
- renderMcpServersList();
3261
3381
  }
3262
3382
  }
3263
3383
  } catch { }
3264
3384
 
3265
- // 2. Update account badge in header
3266
3385
  const badgeEl = getEl('mcp-account-badge');
3267
3386
  if (badgeEl) {
3268
3387
  if (state.user?.email) {
@@ -3273,50 +3392,9 @@ async function loadMcpData() {
3273
3392
  }
3274
3393
  }
3275
3394
 
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 { }
3312
- }
3313
-
3314
- renderMcpKpis();
3315
- renderMcpServersList();
3316
- renderMcpActivityList();
3317
- } catch (err) {
3318
- console.error('Failed to load MCP ecosystem data:', err);
3319
- }
3395
+ renderMcpKpis();
3396
+ renderMcpServersList();
3397
+ renderMcpActivityList();
3320
3398
  }
3321
3399
 
3322
3400
  function renderMcpKpis() {
@@ -3361,32 +3439,6 @@ function renderMcpServersList() {
3361
3439
  if (!container) return;
3362
3440
 
3363
3441
  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
-
3390
3442
  container.innerHTML = `
3391
3443
  <div style="padding: 48px 24px; text-align: center;">
3392
3444
  <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;">
@@ -3394,7 +3446,7 @@ function renderMcpServersList() {
3394
3446
  </div>
3395
3447
  <h4 style="font-size: 16px; font-weight: 700; color: #fff; margin: 0 0 8px 0;">No External MCP Servers Connected</h4>
3396
3448
  <p style="font-size: 13px; color: var(--scout-text-secondary); max-width: 480px; margin: 0 auto 20px auto; line-height: 1.5;">
3397
- Connect Scout to standard-compliant MCP servers (Motion, Higgsfield, GitHub, Figma, Slack, or local tools) to provide the autonomous agent with custom tools.
3449
+ Connect Scout to standard-compliant MCP servers (GitHub, Figma, Motion, Slack, or remote API endpoints) to empower the autonomous agent with custom tools.
3398
3450
  </p>
3399
3451
  <button type="button" class="btn btn-primary" onclick="openAddMcpModal()">
3400
3452
  <i class="fa-solid fa-plus"></i> Connect First MCP Server
@@ -3470,7 +3522,7 @@ function renderMcpServersList() {
3470
3522
  </div>
3471
3523
 
3472
3524
  <div class="mcp-server-footer">
3473
- <div style="display: flex; gap: 8px;">
3525
+ <div style="display: flex; gap: 8px; flex-wrap: wrap;">
3474
3526
  ${isConnected ? `
3475
3527
  <button type="button" class="btn btn-xs btn-outline" onclick="disconnectMcpServer('${escapeHtml(server.id)}')">
3476
3528
  <i class="fa-solid fa-power-off"></i> Disconnect
@@ -3483,6 +3535,9 @@ function renderMcpServersList() {
3483
3535
  <button type="button" class="btn btn-xs btn-outline" onclick="openInspectMcpModal('${escapeHtml(server.id)}')">
3484
3536
  <i class="fa-solid fa-magnifying-glass"></i> Tools (${toolsCount})
3485
3537
  </button>
3538
+ <button type="button" class="btn btn-xs btn-outline" title="Copy CLI command to connect this server in your terminal" onclick="copyMcpCliCommand('${escapeHtml(server.id)}')">
3539
+ <i class="fa-solid fa-terminal text-primary"></i> Copy CLI Command
3540
+ </button>
3486
3541
  </div>
3487
3542
  <button type="button" class="btn btn-xs btn-outline text-danger" title="Remove MCP Server" onclick="deleteMcpServer('${escapeHtml(server.id)}')">
3488
3543
  <i class="fa-solid fa-trash"></i>
@@ -3641,52 +3696,68 @@ async function handleAddMcpServerSubmit(e) {
3641
3696
  autoConnect,
3642
3697
  };
3643
3698
 
3644
- const apiBase = getMcpApiBase();
3645
- const canCallApi = state.serverConnected || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || (apiBase && apiBase.length > 0);
3699
+ const tempServer = {
3700
+ name,
3701
+ transport,
3702
+ endpoint: transport === 'http' ? endpoint : undefined,
3703
+ command: transport === 'stdio' ? command : undefined,
3704
+ args: transport === 'stdio' ? args : undefined,
3705
+ authType,
3706
+ headerKey: authType === 'apiKey' ? headerKey : undefined,
3707
+ token: authType !== 'none' ? token : undefined,
3708
+ };
3646
3709
 
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
- });
3710
+ const discovered = await connectRemoteMcpInBrowser(tempServer);
3653
3711
 
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();
3712
+ const newServer = {
3713
+ id: `mcp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
3714
+ name,
3715
+ transport,
3716
+ endpoint: transport === 'http' ? endpoint : undefined,
3717
+ command: transport === 'stdio' ? command : undefined,
3718
+ args: transport === 'stdio' ? args : undefined,
3719
+ authType,
3720
+ headerKey: authType === 'apiKey' ? headerKey : undefined,
3721
+ status: 'connected',
3722
+ userId,
3723
+ userEmail: userEmail || 'default',
3724
+ tools: discovered.tools,
3725
+ health: {
3726
+ latencyMs: discovered.latencyMs,
3727
+ status: 'healthy',
3728
+ lastChecked: new Date().toISOString()
3729
+ },
3730
+ permissions: {
3731
+ autoApproveSafe,
3732
+ requireConfirmationForDestructive
3733
+ },
3734
+ createdAt: new Date().toISOString()
3735
+ };
3736
+
3737
+ mcpState.servers.push(newServer);
3738
+ const storageKey = getAccountMcpStorageKey();
3739
+ try {
3682
3740
  localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
3683
3741
  localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
3684
- showToast(`MCP server "${name}" saved for ${state.user?.email || 'your account'}.`, 'success');
3685
- closeAddMcpModal();
3686
- getEl('form-add-mcp')?.reset();
3687
- renderMcpKpis();
3688
- renderMcpServersList();
3689
- }
3742
+ } catch { }
3743
+
3744
+ // Record handshake in audit log
3745
+ mcpState.activity.unshift({
3746
+ id: `mcp_act_${Date.now()}`,
3747
+ serverId: newServer.id,
3748
+ serverName: newServer.name,
3749
+ toolName: 'mcp_handshake',
3750
+ status: 'success',
3751
+ durationMs: discovered.latencyMs,
3752
+ timestamp: new Date().toISOString()
3753
+ });
3754
+
3755
+ showToast(`Connected to MCP server "${name}"! Click "Copy CLI Command" on the card to run it in your terminal.`, 'success', 6000);
3756
+ closeAddMcpModal();
3757
+ getEl('form-add-mcp')?.reset();
3758
+ renderMcpKpis();
3759
+ renderMcpServersList();
3760
+ renderMcpActivityList();
3690
3761
  } catch (err) {
3691
3762
  showToast(err.message || 'Error connecting MCP server', 'error');
3692
3763
  } finally {
@@ -3760,91 +3831,154 @@ function closeInspectMcpModal() {
3760
3831
  }
3761
3832
 
3762
3833
  async function connectMcpServer(serverId) {
3834
+ const server = mcpState.servers.find(s => s.id === serverId);
3835
+ if (!server) {
3836
+ showToast('Server not found', 'error');
3837
+ return;
3838
+ }
3839
+
3763
3840
  try {
3764
- showToast('Connecting to MCP server...', 'info');
3765
- const res = await fetch(`${getMcpApiBase()}/api/mcp/servers/${encodeURIComponent(serverId)}/connect`, { method: 'POST' });
3766
- const data = await res.json();
3767
- if (data.success) {
3768
- showToast(`Connected! Discovered ${data.server.tools?.length || 0} tools.`, 'success');
3769
- await loadMcpData();
3770
- } else {
3771
- showToast(data.error || 'Failed to connect server', 'error');
3772
- }
3841
+ server.status = 'connecting';
3842
+ renderMcpServersList();
3843
+ showToast(`Connecting to ${server.name}...`, 'info');
3844
+
3845
+ const discovered = await connectRemoteMcpInBrowser(server);
3846
+ server.status = 'connected';
3847
+ server.tools = discovered.tools;
3848
+ server.health = {
3849
+ latencyMs: discovered.latencyMs,
3850
+ status: 'healthy',
3851
+ lastChecked: new Date().toISOString()
3852
+ };
3853
+
3854
+ const storageKey = getAccountMcpStorageKey();
3855
+ try {
3856
+ localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
3857
+ localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
3858
+ } catch { }
3859
+
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
+ renderMcpKpis();
3871
+ renderMcpServersList();
3872
+ renderMcpActivityList();
3873
+ showToast(`Connected to ${server.name}! ${discovered.tools.length} tools active.`, 'success');
3773
3874
  } catch (err) {
3875
+ server.status = 'error';
3876
+ renderMcpServersList();
3774
3877
  showToast(err.message || 'Error connecting to server', 'error');
3775
3878
  }
3776
3879
  }
3777
3880
 
3778
3881
  async function disconnectMcpServer(serverId) {
3779
- try {
3780
- const res = await fetch(`${getMcpApiBase()}/api/mcp/servers/${encodeURIComponent(serverId)}/disconnect`, { method: 'POST' });
3781
- const data = await res.json();
3782
- if (data.success) {
3783
- showToast('MCP server disconnected.', 'info');
3784
- await loadMcpData();
3785
- } else {
3786
- showToast(data.error || 'Failed to disconnect server', 'error');
3787
- }
3788
- } catch (err) {
3789
- showToast(err.message || 'Error disconnecting server', 'error');
3882
+ const server = mcpState.servers.find(s => s.id === serverId);
3883
+ if (server) {
3884
+ server.status = 'disconnected';
3885
+ const storageKey = getAccountMcpStorageKey();
3886
+ try {
3887
+ localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
3888
+ localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
3889
+ } catch { }
3890
+ renderMcpKpis();
3891
+ renderMcpServersList();
3790
3892
  }
3893
+ showToast('MCP server disconnected.', 'info');
3791
3894
  }
3792
3895
 
3793
3896
  async function deleteMcpServer(serverId) {
3794
3897
  if (!confirm('Are you sure you want to remove this MCP server? Associated credentials will be permanently deleted.')) {
3795
3898
  return;
3796
3899
  }
3900
+ mcpState.servers = mcpState.servers.filter(s => s.id !== serverId);
3901
+ const storageKey = getAccountMcpStorageKey();
3797
3902
  try {
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
- }
3809
- } else {
3810
- showToast('MCP server removed from your account.', 'info');
3811
- }
3903
+ localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
3904
+ localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
3905
+ } catch { }
3906
+ renderMcpKpis();
3907
+ renderMcpServersList();
3908
+ showToast('MCP server removed.', 'info');
3909
+ }
3812
3910
 
3813
- mcpState.servers = mcpState.servers.filter(s => s.id !== serverId);
3911
+ async function toggleMcpTool(serverId, toolName, enabled) {
3912
+ const server = mcpState.servers.find(s => s.id === serverId);
3913
+ if (server && server.tools) {
3914
+ const target = server.tools.find(t => t.originalName === toolName || t.name === toolName);
3915
+ if (target) target.enabled = enabled;
3814
3916
  const storageKey = getAccountMcpStorageKey();
3815
3917
  try {
3816
3918
  localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
3817
3919
  localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
3818
3920
  } catch { }
3819
- renderMcpKpis();
3820
- renderMcpServersList();
3821
- } catch (err) {
3822
- showToast(err.message || 'Error deleting server', 'error');
3823
3921
  }
3922
+ showToast(`Tool "${toolName}" ${enabled ? 'enabled' : 'disabled'}.`, 'info');
3923
+ renderMcpKpis();
3924
+ renderMcpServersList();
3824
3925
  }
3825
3926
 
3826
- async function toggleMcpTool(serverId, toolName, enabled) {
3827
- try {
3828
- const res = await fetch(`${getMcpApiBase()}/api/mcp/servers/${encodeURIComponent(serverId)}/tools/${encodeURIComponent(toolName)}/toggle`, {
3829
- method: 'POST',
3830
- headers: { 'Content-Type': 'application/json' },
3831
- body: JSON.stringify({ enabled })
3832
- });
3833
- const data = await res.json();
3834
- if (data.success) {
3835
- showToast(`Tool "${toolName}" ${enabled ? 'enabled' : 'disabled'}.`, 'info');
3836
- const server = mcpState.servers.find(s => s.id === serverId);
3837
- if (server && server.tools) {
3838
- const target = server.tools.find(t => t.originalName === toolName || t.name === toolName);
3839
- if (target) target.enabled = enabled;
3840
- }
3841
- renderMcpKpis();
3842
- renderMcpServersList();
3843
- } else {
3844
- showToast(data.error || 'Failed to toggle tool', 'error');
3927
+ function copyMcpCliCommand(serverId) {
3928
+ const server = mcpState.servers.find(s => s.id === serverId);
3929
+ if (!server) {
3930
+ showToast('Server not found.', 'error');
3931
+ return;
3932
+ }
3933
+ let cmd = `scout mcp add "${server.name}" --transport ${server.transport}`;
3934
+ if (server.transport === 'http' && server.endpoint) {
3935
+ cmd += ` --endpoint "${server.endpoint}"`;
3936
+ if (server.authType && server.authType !== 'none') {
3937
+ cmd += ` --auth-type ${server.authType}`;
3845
3938
  }
3846
- } catch (err) {
3847
- showToast(err.message || 'Error toggling tool', 'error');
3939
+ } else if (server.transport === 'stdio' && server.command) {
3940
+ cmd += ` --command "${server.command}"`;
3941
+ if (server.args && server.args.length > 0) {
3942
+ cmd += ` --args "${server.args.join(' ')}"`;
3943
+ }
3944
+ }
3945
+ if (navigator.clipboard && navigator.clipboard.writeText) {
3946
+ navigator.clipboard.writeText(cmd).then(() => {
3947
+ showToast('Copied CLI command to clipboard! Paste and run in terminal to activate.', 'success');
3948
+ }).catch(() => {
3949
+ prompt('Copy this CLI command for terminal:', cmd);
3950
+ });
3951
+ } else {
3952
+ prompt('Copy this CLI command for terminal:', cmd);
3953
+ }
3954
+ }
3955
+
3956
+ function exportMcpCliConfig() {
3957
+ if (!mcpState.servers || mcpState.servers.length === 0) {
3958
+ showToast('No MCP servers configured to export.', 'info');
3959
+ return;
3960
+ }
3961
+ const cliFormat = mcpState.servers.map(s => ({
3962
+ id: s.id || s.name.toLowerCase().replace(/[^a-z0-9_-]/g, '_'),
3963
+ name: s.name,
3964
+ transport: s.transport,
3965
+ endpoint: s.endpoint,
3966
+ command: s.command,
3967
+ args: s.args || [],
3968
+ authType: s.authType || 'none',
3969
+ status: s.status || 'connected',
3970
+ tools: s.tools || [],
3971
+ health: s.health || { status: 'connected', latencyMs: 150, lastCheckedAt: new Date().toISOString() }
3972
+ }));
3973
+ const jsonStr = JSON.stringify(cliFormat, null, 2);
3974
+ if (navigator.clipboard && navigator.clipboard.writeText) {
3975
+ navigator.clipboard.writeText(jsonStr).then(() => {
3976
+ showToast('Copied ~/.ft/mcp_servers.json configuration to clipboard!', 'success');
3977
+ }).catch(() => {
3978
+ prompt('Copy JSON for ~/.ft/mcp_servers.json:', jsonStr);
3979
+ });
3980
+ } else {
3981
+ prompt('Copy JSON for ~/.ft/mcp_servers.json:', jsonStr);
3848
3982
  }
3849
3983
  }
3850
3984
 
@@ -3866,6 +4000,8 @@ window.connectMcpServer = connectMcpServer;
3866
4000
  window.disconnectMcpServer = disconnectMcpServer;
3867
4001
  window.deleteMcpServer = deleteMcpServer;
3868
4002
  window.toggleMcpTool = toggleMcpTool;
4003
+ window.copyMcpCliCommand = copyMcpCliCommand;
4004
+ window.exportMcpCliConfig = exportMcpCliConfig;
3869
4005
 
3870
4006
 
3871
4007