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/.firebase/hosting.d2Vi.cache +15 -8
- package/README.md +2 -2
- 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 +1 -32
- 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 +4 -5
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/engine/runtime/agentRuntime.js +1 -1
- package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
- package/bin/src/index.js +71 -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 +24 -1
- package/bin/src/mcp/manager.js.map +1 -1
- package/bin/src/mcp/toolRouter.d.ts.map +1 -1
- package/bin/src/mcp/toolRouter.js +8 -2
- package/bin/src/mcp/toolRouter.js.map +1 -1
- package/bin/src/server/server.d.ts.map +1 -1
- package/bin/src/server/server.js +3 -0
- package/bin/src/server/server.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/package.json +1 -1
- package/web/ScoutOGImage.png +0 -0
- package/web/app.js +354 -218
- package/web/ft-logo.png +0 -0
- package/web/og-image.png +0 -0
- package/web/robots.txt +24 -0
- package/web/site.webmanifest +16 -0
- package/web/sitemap-tryscout.xml +47 -0
- package/web/sitemap.xml +47 -0
- 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.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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
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 (
|
|
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
|
|
3645
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
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
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
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
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
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
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
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
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
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
|
-
}
|
|
3847
|
-
|
|
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
|
|