ft-scout 9.0.8 → 9.1.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/.firebase/hosting.d2Vi.cache +8 -8
- package/README.md +2 -2
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +26 -13
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/commands/mcp.d.ts.map +1 -1
- package/bin/src/commands/mcp.js +7 -4
- package/bin/src/commands/mcp.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +58 -52
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/llm.d.ts.map +1 -1
- package/bin/src/engine/llm.js +2 -4
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/index.js +70 -25
- package/bin/src/index.js.map +1 -1
- package/bin/src/mcp/client.js +1 -1
- package/bin/src/mcp/manager.d.ts.map +1 -1
- package/bin/src/mcp/manager.js +140 -4
- package/bin/src/mcp/manager.js.map +1 -1
- package/bin/src/mcp/persistence.d.ts.map +1 -1
- package/bin/src/mcp/persistence.js +15 -0
- package/bin/src/mcp/persistence.js.map +1 -1
- package/bin/src/mcp/types.d.ts.map +1 -1
- package/bin/src/server/server.d.ts.map +1 -1
- package/bin/src/server/server.js +18 -12
- package/bin/src/server/server.js.map +1 -1
- package/bin/src/utils/admin.d.ts.map +1 -1
- package/bin/src/utils/admin.js +5 -11
- package/bin/src/utils/admin.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/bin/src/utils/session.d.ts.map +1 -1
- package/bin/src/utils/session.js +8 -7
- package/bin/src/utils/session.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +466 -108
- package/web/styles.css +2 -2
package/web/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
FrontTerrain Scout Multi-Page Application Engine v9.0
|
|
2
|
+
FrontTerrain Scout Multi-Page Application Engine v9.1.0
|
|
3
3
|
========================================================================== */
|
|
4
4
|
|
|
5
5
|
const FIREBASE_CONFIG = {
|
|
@@ -218,19 +218,49 @@ async function loadDynamicPricing() {
|
|
|
218
218
|
}
|
|
219
219
|
} catch { }
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
221
|
+
checkAdminStatus();
|
|
222
|
+
}
|
|
223
|
+
|
|
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();
|
|
231
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));
|
|
232
257
|
}
|
|
233
|
-
|
|
258
|
+
showAdminNavigationLink();
|
|
259
|
+
return true;
|
|
260
|
+
} else {
|
|
261
|
+
hideAdminNavigationLink();
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
234
264
|
}
|
|
235
265
|
|
|
236
266
|
function showAdminNavigationLink() {
|
|
@@ -238,6 +268,7 @@ function showAdminNavigationLink() {
|
|
|
238
268
|
const adminGroup = document.getElementById('nav-group-admin');
|
|
239
269
|
const topbarAdmin = document.getElementById('topbar-admin-link');
|
|
240
270
|
const dropdownAdmin = document.getElementById('btn-dropdown-admin');
|
|
271
|
+
const settingsAdminBtn = document.getElementById('settings-admin-portal-btn');
|
|
241
272
|
|
|
242
273
|
if (adminGroup) adminGroup.style.display = 'block';
|
|
243
274
|
if (adminLink) {
|
|
@@ -246,10 +277,11 @@ function showAdminNavigationLink() {
|
|
|
246
277
|
}
|
|
247
278
|
if (topbarAdmin) topbarAdmin.style.display = 'inline-flex';
|
|
248
279
|
if (dropdownAdmin) dropdownAdmin.style.display = 'flex';
|
|
280
|
+
if (settingsAdminBtn) settingsAdminBtn.style.display = 'inline-flex';
|
|
249
281
|
|
|
250
282
|
// Highlight admin identity in sidebar user footer
|
|
251
283
|
const sidebarTier = document.getElementById('sidebar-user-tier');
|
|
252
|
-
if (sidebarTier
|
|
284
|
+
if (sidebarTier) {
|
|
253
285
|
sidebarTier.innerHTML = '<span style="color: #e15b37; font-weight: 700;"><i class="fa-solid fa-shield-halved"></i> Master Admin</span>';
|
|
254
286
|
}
|
|
255
287
|
|
|
@@ -266,6 +298,20 @@ function showAdminNavigationLink() {
|
|
|
266
298
|
}
|
|
267
299
|
}
|
|
268
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
|
+
|
|
269
315
|
// Initialize Application
|
|
270
316
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
271
317
|
highlightActiveNav();
|
|
@@ -1057,7 +1103,7 @@ function initTerminalSimulator() {
|
|
|
1057
1103
|
`;
|
|
1058
1104
|
} else if (lower.includes('status')) {
|
|
1059
1105
|
response.innerHTML = `
|
|
1060
|
-
<span class="term-green">✔ Scout CLI Engine: v9.0
|
|
1106
|
+
<span class="term-green">✔ Scout CLI Engine: v9.1.0 Online</span><br>
|
|
1061
1107
|
↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
|
|
1062
1108
|
↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
|
|
1063
1109
|
↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
|
|
@@ -1304,7 +1350,7 @@ function renderTiersUI() {
|
|
|
1304
1350
|
getEl('price-pro-val').innerText = proUSD;
|
|
1305
1351
|
}
|
|
1306
1352
|
}
|
|
1307
|
-
if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isProOffer ? `/ month (${p.pro?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '(billed annually)' : '/ month');
|
|
1353
|
+
if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isProOffer ? `/ month (${p.pro?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '/month(billed annually)' : '/ month');
|
|
1308
1354
|
const proCurrency = document.querySelector('#tier-card-pro .price-currency');
|
|
1309
1355
|
if (proCurrency) proCurrency.innerText = isProOffer ? '' : '$';
|
|
1310
1356
|
|
|
@@ -1315,7 +1361,7 @@ function renderTiersUI() {
|
|
|
1315
1361
|
getEl('price-ent-val').innerText = entUSD;
|
|
1316
1362
|
}
|
|
1317
1363
|
}
|
|
1318
|
-
if (getEl('price-ent-period')) getEl('price-ent-period').innerText = isEntOffer ? `/ month (${p.enterprise?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '(billed annually)' : '/ month');
|
|
1364
|
+
if (getEl('price-ent-period')) getEl('price-ent-period').innerText = isEntOffer ? `/ month (${p.enterprise?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '/month(billed annually)' : '/ month');
|
|
1319
1365
|
const entCurrency = document.querySelector('#tier-card-enterprise .price-currency');
|
|
1320
1366
|
if (entCurrency) entCurrency.innerText = isEntOffer ? '' : '$';
|
|
1321
1367
|
|
|
@@ -1367,8 +1413,8 @@ async function checkServerStatusAndSession() {
|
|
|
1367
1413
|
let connected = false;
|
|
1368
1414
|
let data = null;
|
|
1369
1415
|
|
|
1370
|
-
// Only attempt
|
|
1371
|
-
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
1416
|
+
// Only attempt direct relative path if running locally or via file protocol
|
|
1417
|
+
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.protocol === 'file:';
|
|
1372
1418
|
|
|
1373
1419
|
if (isLocalhost) {
|
|
1374
1420
|
try {
|
|
@@ -1381,15 +1427,22 @@ async function checkServerStatusAndSession() {
|
|
|
1381
1427
|
}
|
|
1382
1428
|
}
|
|
1383
1429
|
} catch { }
|
|
1430
|
+
}
|
|
1384
1431
|
|
|
1385
|
-
|
|
1432
|
+
if (!connected) {
|
|
1433
|
+
const candidateHosts = ['http://localhost:9120', 'http://127.0.0.1:9120', 'http://localhost:9121', 'http://localhost:9122'];
|
|
1434
|
+
for (const host of candidateHosts) {
|
|
1386
1435
|
try {
|
|
1387
|
-
const
|
|
1436
|
+
const controller = new AbortController();
|
|
1437
|
+
const timeoutId = setTimeout(() => controller.abort(), 1200);
|
|
1438
|
+
const res = await fetch(`${host}/api/status`, { method: 'GET', signal: controller.signal });
|
|
1439
|
+
clearTimeout(timeoutId);
|
|
1388
1440
|
if (res.ok) {
|
|
1389
1441
|
data = await res.json();
|
|
1390
1442
|
if (data && data.status === 'ok') {
|
|
1391
|
-
state.apiBaseUrl =
|
|
1443
|
+
state.apiBaseUrl = host;
|
|
1392
1444
|
connected = true;
|
|
1445
|
+
break;
|
|
1393
1446
|
}
|
|
1394
1447
|
}
|
|
1395
1448
|
} catch { }
|
|
@@ -1475,8 +1528,46 @@ async function handleSignIn(e) {
|
|
|
1475
1528
|
if (!email || !password) return;
|
|
1476
1529
|
|
|
1477
1530
|
// Check Master Admin Credentials
|
|
1478
|
-
const
|
|
1531
|
+
const AUTHORIZED_ADMIN_EMAILS = [
|
|
1532
|
+
'rishishandilya40@gmail.com',
|
|
1533
|
+
'rishi.shandilya@frontterrain.com',
|
|
1534
|
+
'company@frontterrain.com'
|
|
1535
|
+
];
|
|
1536
|
+
const isMasterAdminAuth = AUTHORIZED_ADMIN_EMAILS.includes(email.toLowerCase().trim()) && password === 'scout_admin_2026';
|
|
1479
1537
|
if (isMasterAdminAuth) {
|
|
1538
|
+
if (state.serverConnected) {
|
|
1539
|
+
try {
|
|
1540
|
+
const adminLoginRes = await fetch(`${state.apiBaseUrl}/api/admin/login`, {
|
|
1541
|
+
method: 'POST',
|
|
1542
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1543
|
+
body: JSON.stringify({ email: email.trim(), passkey: password }),
|
|
1544
|
+
});
|
|
1545
|
+
if (adminLoginRes.ok) {
|
|
1546
|
+
const adminData = await adminLoginRes.json();
|
|
1547
|
+
if (adminData.success && adminData.token) {
|
|
1548
|
+
state.token = adminData.token;
|
|
1549
|
+
state.refreshToken = 'rt_' + Date.now().toString(36);
|
|
1550
|
+
state.user = {
|
|
1551
|
+
id: 'usr_admin_001',
|
|
1552
|
+
email: email,
|
|
1553
|
+
name: 'Scout Administrator',
|
|
1554
|
+
createdAt: new Date().toISOString(),
|
|
1555
|
+
};
|
|
1556
|
+
state.currentTier = 'pro';
|
|
1557
|
+
state.creditsTotal = 1000;
|
|
1558
|
+
state.creditsUsed = 0;
|
|
1559
|
+
sessionStorage.setItem('scout_admin_token', adminData.token);
|
|
1560
|
+
saveSessionToStorage();
|
|
1561
|
+
updateUserUI();
|
|
1562
|
+
showToast('Authenticated as Scout Administrator! Redirecting to Control Center...', 'success');
|
|
1563
|
+
setTimeout(() => { window.location.href = '/admin'; }, 400);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
} catch { }
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Standalone fallback
|
|
1480
1571
|
state.token = 'scout_admin_token_' + Date.now().toString(36);
|
|
1481
1572
|
state.refreshToken = 'rt_' + Date.now().toString(36);
|
|
1482
1573
|
state.user = {
|
|
@@ -1735,6 +1826,7 @@ function showDashboard() {
|
|
|
1735
1826
|
if (btnOpenAuth) btnOpenAuth.classList.add('hidden');
|
|
1736
1827
|
|
|
1737
1828
|
renderAllDynamicComponents();
|
|
1829
|
+
checkAdminStatus();
|
|
1738
1830
|
}
|
|
1739
1831
|
|
|
1740
1832
|
function updateUserUI() {
|
|
@@ -1743,6 +1835,7 @@ function updateUserUI() {
|
|
|
1743
1835
|
const btnOpenAuth = getEl('btn-open-auth');
|
|
1744
1836
|
if (userMenu) userMenu.classList.add('hidden');
|
|
1745
1837
|
if (btnOpenAuth) btnOpenAuth.classList.remove('hidden');
|
|
1838
|
+
hideAdminNavigationLink();
|
|
1746
1839
|
return;
|
|
1747
1840
|
}
|
|
1748
1841
|
|
|
@@ -1771,6 +1864,9 @@ function updateUserUI() {
|
|
|
1771
1864
|
if (getEl('settings-active-tier')) getEl('settings-active-tier').value = tierInfo.name;
|
|
1772
1865
|
if (getEl('sidebar-user-tier')) getEl('sidebar-user-tier').innerText = tierInfo.name;
|
|
1773
1866
|
|
|
1867
|
+
// Check & display Admin portal link and Master Admin badge
|
|
1868
|
+
checkAdminStatus();
|
|
1869
|
+
|
|
1774
1870
|
if (getEl('select-byok-provider')) getEl('select-byok-provider').value = state.customLlmProvider || 'nvidia';
|
|
1775
1871
|
if (getEl('input-byok-endpoint')) getEl('input-byok-endpoint').value = state.customBaseUrl || '';
|
|
1776
1872
|
if (getEl('input-byok-key')) getEl('input-byok-key').value = state.customApiKey || '';
|
|
@@ -1784,6 +1880,11 @@ function updateUserUI() {
|
|
|
1784
1880
|
inceptionBadge.style.display = 'none';
|
|
1785
1881
|
}
|
|
1786
1882
|
}
|
|
1883
|
+
|
|
1884
|
+
// Refresh MCP servers scoped to active user
|
|
1885
|
+
if (typeof loadMcpData === 'function') {
|
|
1886
|
+
loadMcpData();
|
|
1887
|
+
}
|
|
1787
1888
|
}
|
|
1788
1889
|
|
|
1789
1890
|
async function handleLogout() {
|
|
@@ -1797,10 +1898,18 @@ async function handleLogout() {
|
|
|
1797
1898
|
state.token = null;
|
|
1798
1899
|
state.refreshToken = null;
|
|
1799
1900
|
|
|
1901
|
+
sessionStorage.removeItem('scout_admin_token');
|
|
1902
|
+
hideAdminNavigationLink();
|
|
1903
|
+
|
|
1800
1904
|
localStorage.removeItem('scout_auth_user');
|
|
1801
1905
|
localStorage.removeItem('scout_auth_token');
|
|
1802
1906
|
localStorage.removeItem('scout_refresh_token');
|
|
1803
1907
|
|
|
1908
|
+
if (typeof mcpState !== 'undefined') {
|
|
1909
|
+
mcpState.servers = [];
|
|
1910
|
+
if (typeof renderMcpServersList === 'function') renderMcpServersList();
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1804
1913
|
showToast('Signed out of Scout session.', 'info');
|
|
1805
1914
|
window.location.href = '/auth';
|
|
1806
1915
|
}
|
|
@@ -3142,11 +3251,117 @@ function initWorkstationRouter() {
|
|
|
3142
3251
|
}
|
|
3143
3252
|
|
|
3144
3253
|
// ==========================================================================
|
|
3145
|
-
// Model Context Protocol (MCP) Ecosystem Controller
|
|
3254
|
+
// Model Context Protocol (MCP) Cloud Ecosystem Controller
|
|
3146
3255
|
// ==========================================================================
|
|
3147
3256
|
|
|
3257
|
+
function getAccountMcpStorageKey() {
|
|
3258
|
+
const email = (state.user?.email || '').trim().toLowerCase();
|
|
3259
|
+
return email ? `scout_mcp_servers_${email}` : 'scout_cached_mcp_servers';
|
|
3260
|
+
}
|
|
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
|
+
|
|
3148
3355
|
const mcpState = {
|
|
3149
|
-
servers:
|
|
3356
|
+
servers: (() => {
|
|
3357
|
+
try {
|
|
3358
|
+
const key = (state.user?.email) ? `scout_mcp_servers_${state.user.email.toLowerCase()}` : 'scout_cached_mcp_servers';
|
|
3359
|
+
const cached = localStorage.getItem(key) || localStorage.getItem('scout_cached_mcp_servers');
|
|
3360
|
+
return cached ? JSON.parse(cached) : [];
|
|
3361
|
+
} catch {
|
|
3362
|
+
return [];
|
|
3363
|
+
}
|
|
3364
|
+
})(),
|
|
3150
3365
|
activity: [],
|
|
3151
3366
|
health: {},
|
|
3152
3367
|
selectedServer: null,
|
|
@@ -3154,29 +3369,32 @@ const mcpState = {
|
|
|
3154
3369
|
};
|
|
3155
3370
|
|
|
3156
3371
|
async function loadMcpData() {
|
|
3157
|
-
|
|
3158
|
-
|
|
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
|
-
]);
|
|
3372
|
+
const userEmail = (state.user?.email || '').trim().toLowerCase();
|
|
3373
|
+
const storageKey = getAccountMcpStorageKey();
|
|
3163
3374
|
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
mcpState.activity = activityRes.activity;
|
|
3375
|
+
try {
|
|
3376
|
+
const cached = localStorage.getItem(storageKey) || localStorage.getItem('scout_cached_mcp_servers');
|
|
3377
|
+
if (cached) {
|
|
3378
|
+
const parsed = JSON.parse(cached);
|
|
3379
|
+
if (Array.isArray(parsed)) {
|
|
3380
|
+
mcpState.servers = parsed;
|
|
3381
|
+
}
|
|
3172
3382
|
}
|
|
3383
|
+
} catch { }
|
|
3173
3384
|
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3385
|
+
const badgeEl = getEl('mcp-account-badge');
|
|
3386
|
+
if (badgeEl) {
|
|
3387
|
+
if (state.user?.email) {
|
|
3388
|
+
badgeEl.style.display = 'inline-flex';
|
|
3389
|
+
badgeEl.innerHTML = `<i class="fa-solid fa-user-check" style="margin-right: 4px;"></i> ${escapeHtml(state.user.email)}`;
|
|
3390
|
+
} else {
|
|
3391
|
+
badgeEl.style.display = 'none';
|
|
3392
|
+
}
|
|
3179
3393
|
}
|
|
3394
|
+
|
|
3395
|
+
renderMcpKpis();
|
|
3396
|
+
renderMcpServersList();
|
|
3397
|
+
renderMcpActivityList();
|
|
3180
3398
|
}
|
|
3181
3399
|
|
|
3182
3400
|
function renderMcpKpis() {
|
|
@@ -3228,7 +3446,7 @@ function renderMcpServersList() {
|
|
|
3228
3446
|
</div>
|
|
3229
3447
|
<h4 style="font-size: 16px; font-weight: 700; color: #fff; margin: 0 0 8px 0;">No External MCP Servers Connected</h4>
|
|
3230
3448
|
<p style="font-size: 13px; color: var(--scout-text-secondary); max-width: 480px; margin: 0 auto 20px auto; line-height: 1.5;">
|
|
3231
|
-
Connect Scout to standard-compliant MCP servers (
|
|
3449
|
+
Connect Scout to standard-compliant MCP servers (GitHub, Figma, Motion, Slack, or remote API endpoints) to empower the autonomous agent with custom tools.
|
|
3232
3450
|
</p>
|
|
3233
3451
|
<button type="button" class="btn btn-primary" onclick="openAddMcpModal()">
|
|
3234
3452
|
<i class="fa-solid fa-plus"></i> Connect First MCP Server
|
|
@@ -3304,7 +3522,7 @@ function renderMcpServersList() {
|
|
|
3304
3522
|
</div>
|
|
3305
3523
|
|
|
3306
3524
|
<div class="mcp-server-footer">
|
|
3307
|
-
<div style="display: flex; gap: 8px;">
|
|
3525
|
+
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
|
3308
3526
|
${isConnected ? `
|
|
3309
3527
|
<button type="button" class="btn btn-xs btn-outline" onclick="disconnectMcpServer('${escapeHtml(server.id)}')">
|
|
3310
3528
|
<i class="fa-solid fa-power-off"></i> Disconnect
|
|
@@ -3317,6 +3535,9 @@ function renderMcpServersList() {
|
|
|
3317
3535
|
<button type="button" class="btn btn-xs btn-outline" onclick="openInspectMcpModal('${escapeHtml(server.id)}')">
|
|
3318
3536
|
<i class="fa-solid fa-magnifying-glass"></i> Tools (${toolsCount})
|
|
3319
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>
|
|
3320
3541
|
</div>
|
|
3321
3542
|
<button type="button" class="btn btn-xs btn-outline text-danger" title="Remove MCP Server" onclick="deleteMcpServer('${escapeHtml(server.id)}')">
|
|
3322
3543
|
<i class="fa-solid fa-trash"></i>
|
|
@@ -3454,6 +3675,9 @@ async function handleAddMcpServerSubmit(e) {
|
|
|
3454
3675
|
}
|
|
3455
3676
|
|
|
3456
3677
|
try {
|
|
3678
|
+
const userEmail = (state.user?.email || '').trim().toLowerCase();
|
|
3679
|
+
const userId = state.user?.id || state.user?.uid || undefined;
|
|
3680
|
+
|
|
3457
3681
|
const payload = {
|
|
3458
3682
|
name,
|
|
3459
3683
|
transport,
|
|
@@ -3463,6 +3687,8 @@ async function handleAddMcpServerSubmit(e) {
|
|
|
3463
3687
|
authType,
|
|
3464
3688
|
headerKey: authType === 'apiKey' ? headerKey : undefined,
|
|
3465
3689
|
token: authType !== 'none' ? token : undefined,
|
|
3690
|
+
userId,
|
|
3691
|
+
userEmail: userEmail || undefined,
|
|
3466
3692
|
permissions: {
|
|
3467
3693
|
autoApproveSafe,
|
|
3468
3694
|
requireConfirmationForDestructive,
|
|
@@ -3470,21 +3696,68 @@ async function handleAddMcpServerSubmit(e) {
|
|
|
3470
3696
|
autoConnect,
|
|
3471
3697
|
};
|
|
3472
3698
|
|
|
3473
|
-
const
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
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
|
+
};
|
|
3709
|
+
|
|
3710
|
+
const discovered = await connectRemoteMcpInBrowser(tempServer);
|
|
3711
|
+
|
|
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 {
|
|
3740
|
+
localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
|
|
3741
|
+
localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
|
|
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()
|
|
3477
3753
|
});
|
|
3478
3754
|
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
} else {
|
|
3486
|
-
showToast(data.error || 'Failed to add MCP server', 'error');
|
|
3487
|
-
}
|
|
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();
|
|
3488
3761
|
} catch (err) {
|
|
3489
3762
|
showToast(err.message || 'Error connecting MCP server', 'error');
|
|
3490
3763
|
} finally {
|
|
@@ -3558,79 +3831,162 @@ function closeInspectMcpModal() {
|
|
|
3558
3831
|
}
|
|
3559
3832
|
|
|
3560
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
|
+
|
|
3561
3840
|
try {
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
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');
|
|
3571
3874
|
} catch (err) {
|
|
3875
|
+
server.status = 'error';
|
|
3876
|
+
renderMcpServersList();
|
|
3572
3877
|
showToast(err.message || 'Error connecting to server', 'error');
|
|
3573
3878
|
}
|
|
3574
3879
|
}
|
|
3575
3880
|
|
|
3576
3881
|
async function disconnectMcpServer(serverId) {
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
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();
|
|
3588
3892
|
}
|
|
3893
|
+
showToast('MCP server disconnected.', 'info');
|
|
3589
3894
|
}
|
|
3590
3895
|
|
|
3591
3896
|
async function deleteMcpServer(serverId) {
|
|
3592
3897
|
if (!confirm('Are you sure you want to remove this MCP server? Associated credentials will be permanently deleted.')) {
|
|
3593
3898
|
return;
|
|
3594
3899
|
}
|
|
3900
|
+
mcpState.servers = mcpState.servers.filter(s => s.id !== serverId);
|
|
3901
|
+
const storageKey = getAccountMcpStorageKey();
|
|
3595
3902
|
try {
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
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
|
+
}
|
|
3910
|
+
|
|
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;
|
|
3916
|
+
const storageKey = getAccountMcpStorageKey();
|
|
3917
|
+
try {
|
|
3918
|
+
localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
|
|
3919
|
+
localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
|
|
3920
|
+
} catch { }
|
|
3921
|
+
}
|
|
3922
|
+
showToast(`Tool "${toolName}" ${enabled ? 'enabled' : 'disabled'}.`, 'info');
|
|
3923
|
+
renderMcpKpis();
|
|
3924
|
+
renderMcpServersList();
|
|
3925
|
+
}
|
|
3926
|
+
|
|
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}`;
|
|
3603
3938
|
}
|
|
3604
|
-
}
|
|
3605
|
-
|
|
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);
|
|
3606
3953
|
}
|
|
3607
3954
|
}
|
|
3608
3955
|
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
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);
|
|
3615
3979
|
});
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
showToast(`Tool "${toolName}" ${enabled ? 'enabled' : 'disabled'}.`, 'info');
|
|
3619
|
-
const server = mcpState.servers.find(s => s.id === serverId);
|
|
3620
|
-
if (server && server.tools) {
|
|
3621
|
-
const target = server.tools.find(t => t.originalName === toolName || t.name === toolName);
|
|
3622
|
-
if (target) target.enabled = enabled;
|
|
3623
|
-
}
|
|
3624
|
-
renderMcpKpis();
|
|
3625
|
-
renderMcpServersList();
|
|
3626
|
-
} else {
|
|
3627
|
-
showToast(data.error || 'Failed to toggle tool', 'error');
|
|
3628
|
-
}
|
|
3629
|
-
} catch (err) {
|
|
3630
|
-
showToast(err.message || 'Error toggling tool', 'error');
|
|
3980
|
+
} else {
|
|
3981
|
+
prompt('Copy JSON for ~/.ft/mcp_servers.json:', jsonStr);
|
|
3631
3982
|
}
|
|
3632
3983
|
}
|
|
3633
3984
|
|
|
3985
|
+
// Auto-sync when window regains focus (e.g., returning from CLI terminal)
|
|
3986
|
+
window.addEventListener('focus', () => {
|
|
3987
|
+
loadMcpData();
|
|
3988
|
+
});
|
|
3989
|
+
|
|
3634
3990
|
// Global exports for inline HTML event handlers
|
|
3635
3991
|
window.loadMcpData = loadMcpData;
|
|
3636
3992
|
window.openAddMcpModal = openAddMcpModal;
|
|
@@ -3644,6 +4000,8 @@ window.connectMcpServer = connectMcpServer;
|
|
|
3644
4000
|
window.disconnectMcpServer = disconnectMcpServer;
|
|
3645
4001
|
window.deleteMcpServer = deleteMcpServer;
|
|
3646
4002
|
window.toggleMcpTool = toggleMcpTool;
|
|
4003
|
+
window.copyMcpCliCommand = copyMcpCliCommand;
|
|
4004
|
+
window.exportMcpCliConfig = exportMcpCliConfig;
|
|
3647
4005
|
|
|
3648
4006
|
|
|
3649
4007
|
|